/**
 * Initialise package and set version
 */
ExtMVC = Ext.extend(Ext.util.Observable, {
  version: "0.5b1",
  
  constructor: function() {
    ExtMVC.superclass.constructor.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event environment-changed
       * Fired whenever the environment is changed
       * @param {String} name The name of the new environment
       * @param {Object} config The configuration of the new environment
       */
      'environment-changed'
    );
    
    /**
     * Set up aliases
     */
    this.getEnvSettings = this.getCurrentEnvironmentSettings;
  },
  
  /**
   * @property currentEnvironment
   * @type String
   * The current code environment (defaults to production).  Read-only - use setCurrentEnvironment to change
   */
  currentEnvironment: 'production',
  
  /**
   * Used internally to manage environment variables... user addEnvironmentSettings and 
   * getEnvironmentSettings to change
   */
  environments: {'production': {}},
  
  /**
   * Sets the MVC environment to the specified name.  Usually one of 'production' or 'development'
   * Ignored if the environment has not yet been defined
   * @param {String} name The name of the environment to set.
   */
  setCurrentEnvironment: function(name) {
    if (this.getEnvironmentSettings(name)) {
      this.currentEnvironment = name;
      this.fireEvent('environment-changed', name, this.getEnvironmentSettings(name));
    }
  },
  
  /**
   * Returns the name of the current environment
   * @return {String} The name of the current environment
   */
  getCurrentEnvironment: function() {
    return ExtMVC.currentEnvironment;
  },
  
  /**
   * Returns settings for the current environment (aliased as getEnvSettings)
   * @return {Object} The settings for the current environment
   */
  getCurrentEnvironmentSettings: function() {
    return this.getEnvironmentSettings(this.getCurrentEnvironment());
  },
  
  /**
   * Adds settings for a given environment name
   * @param {String} name The name of the environment to add settings for
   * @param {Object} config The settings object to apply to this environment
   */
  addEnvironmentSettings: function(name, config) {
    ExtMVC.environments[name] = ExtMVC.environments[name] || {};
    Ext.apply(ExtMVC.environments[name], config);
  },
  
  /**
   * Retrieves all settings for a given environment (defaults to the current environment)
   * @param {String} name The name of the environment to get settings from
   * @return {Object} The settings object for the given environment, or null if not found
   */
  getEnvironmentSettings: function(name) {
    name = name || ExtMVC.environment;
    return ExtMVC.environments[name];
  }
});

ExtMVC = new ExtMVC();

Ext.ns('ExtMVC.Model', 'ExtMVC.plugin', 'ExtMVC.view', 'ExtMVC.view.scaffold');

/*
 * Adapted from http://snippets.dzone.com/posts/show/3205
 */

ExtMVC.Inflector = {
  /*
   * The order of all these lists has been reversed from the way 
   * ActiveSupport had them to keep the correct priority.
   */
  Inflections: {
    plural: [
      [(/(quiz)$/i),               "$1zes"  ],
      [(/^(ox)$/i),                "$1en"   ],
      [(/([m|l])ouse$/i),          "$1ice"  ],
      [(/(matr|vert|ind)ix|ex$/i), "$1ices" ],
      [(/(x|ch|ss|sh)$/i),         "$1es"   ],
      [(/([^aeiouy]|qu)y$/i),      "$1ies"  ],
      [(/(hive)$/i),               "$1s"    ],
      [(/(?:([^f])fe|([lr])f)$/i), "$1$2ves"],
      [(/sis$/i),                  "ses"    ],
      [(/([ti])um$/i),             "$1a"    ],
      [(/(buffal|tomat)o$/i),      "$1oes"  ],
      [(/(bu)s$/i),                "$1ses"  ],
      [(/(alias|status)$/i),       "$1es"   ],
      [(/(octop|vir)us$/i),        "$1i"    ],
      [(/(ax|test)is$/i),          "$1es"   ],
      [(/s$/i),                    "s"      ],
      [(/$/),                      "s"      ]
    ],
    singular: [
      [(/(quiz)zes$/i),                                                    "$1"     ],
      [(/(matr)ices$/i),                                                   "$1ix"   ],
      [(/(vert|ind)ices$/i),                                               "$1ex"   ],
      [(/^(ox)en/i),                                                       "$1"     ],
      [(/(alias|status)es$/i),                                             "$1"     ],
      [(/(octop|vir)i$/i),                                                 "$1us"   ],
      [(/(cris|ax|test)es$/i),                                             "$1is"   ],
      [(/(shoe)s$/i),                                                      "$1"     ],
      [(/(o)es$/i),                                                        "$1"     ],
      [(/(bus)es$/i),                                                      "$1"     ],
      [(/([m|l])ice$/i),                                                   "$1ouse" ],
      [(/(x|ch|ss|sh)es$/i),                                               "$1"     ],
      [(/(m)ovies$/i),                                                     "$1ovie" ],
      [(/(s)eries$/i),                                                     "$1eries"],
      [(/([^aeiouy]|qu)ies$/i),                                            "$1y"    ],
      [(/([lr])ves$/i),                                                    "$1f"    ],
      [(/(tive)s$/i),                                                      "$1"     ],
      [(/(hive)s$/i),                                                      "$1"     ],
      [(/([^f])ves$/i),                                                    "$1fe"   ],
      [(/(^analy)ses$/i),                                                  "$1sis"  ],
      [(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i), "$1$2sis"],
      [(/([ti])a$/i),                                                      "$1um"   ],
      [(/(n)ews$/i),                                                       "$1ews"  ],
      [(/s$/i),                                                            ""       ]
    ],
    irregular: [
      ['move',   'moves'   ],
      ['sex',    'sexes'   ],
      ['child',  'children'],
      ['man',    'men'     ],
      ['person', 'people'  ]
    ],
    uncountable: [
      "sheep",
      "fish",
      "series",
      "species",
      "money",
      "rice",
      "information",
      "equipment"
    ]
  },
  
  ordinalize: function(number) {
    if (11 <= parseInt(number, 10) % 100 && parseInt(number, 10) % 100 <= 13) {
      return number + "th";
    } else {
      switch (parseInt(number, 10) % 10) {
        case  1: return number + "st";
        case  2: return number + "nd";
        case  3: return number + "rd";
        default: return number + "th";
      }
    }
  },
  
  pluralize: function(word) {
    var unusual = ExtMVC.Inflector.uncountableOrIrregular(word);
    if (unusual) { return unusual; }
    
    for (var i = 0; i < ExtMVC.Inflector.Inflections.plural.length; i++) {
      var regex          = ExtMVC.Inflector.Inflections.plural[i][0];
      var replace_string = ExtMVC.Inflector.Inflections.plural[i][1];
      if (regex.test(word)) {
        return word.replace(regex, replace_string);
      }
    }
    
    //return the word if it is already pluralized
    return word;
  },
  
  singularize: function(word) {
    var unusual = ExtMVC.Inflector.uncountableOrIrregular(word);
    if (unusual) { return unusual; }
    
    for (var i = 0; i < ExtMVC.Inflector.Inflections.singular.length; i++) {
      var regex          = ExtMVC.Inflector.Inflections.singular[i][0];
      var replace_string = ExtMVC.Inflector.Inflections.singular[i][1];
      
      if (regex.test(word)) {
        return word.replace(regex, replace_string);
      }
    }
    
    //return the word if it is already singularized
    return word;
  },
  
  uncountableOrIrregular: function(word) {
    for (var i = 0; i < ExtMVC.Inflector.Inflections.uncountable.length; i++) {
      var uncountable = ExtMVC.Inflector.Inflections.uncountable[i];
      if (word.toLowerCase == uncountable) {
        return uncountable;
      }
    }
    for (var i = 0; i < ExtMVC.Inflector.Inflections.irregular.length; i++) {
      var singular = ExtMVC.Inflector.Inflections.irregular[i][0];
      var plural   = ExtMVC.Inflector.Inflections.irregular[i][1];
      if ((word.toLowerCase == singular) || (word == plural)) {
        return plural;
      }
    }
    
    return false;
  }
};

/**
 * @returns A capitalized string (e.g. "some test sentence" becomes "Some test sentence")
 * @type String
 */
String.prototype.capitalize = function() {
  return this.charAt(0).toUpperCase() + this.substr(1).toLowerCase();
};

/**
 * @returns The string in Title Case (e.g. "some test sentence" becomes "Some Test Sentence")
 * @type String
 */
String.prototype.titleize = function() {
  return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
};

/**
 * Takes any string and de-underscores and uppercases it
 * e.g. long_underscored_string => LongUnderscoredString
 */
String.prototype.camelize = function() {
  return this.replace(/_/g, " ").titleize().replace(/ /g, "");
};

String.prototype.underscore = function() {
  return this.toLowerCase().replace(/ /g, "_");
};

String.prototype.singularize = function() {
  return ExtMVC.Inflector.singularize(this);
};

String.prototype.pluralize = function() {
  return ExtMVC.Inflector.pluralize(this);
};

String.prototype.escapeHTML = function () {                                       
  return this.replace(/&/g,'&amp;').replace(/>/g,'&gt;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
};

/**
 * Converts this string into a currency, prepended with the given currency symbol
 * @param {String} symbol The currency symbol to use (defaults to $)
 */
String.prototype.toCurrency = function(symbol) {
  if (typeof(symbol) == 'undefined') {var symbol = '$';}
  
  var beforeDecimal = this.split(".")[0],
      afterDecimal  = this.split(".")[1];
  
  var segmentCount      = Math.floor(beforeDecimal.length / 3);
  var firstSegmentWidth = beforeDecimal.length % 3,
      pointerPosition   = firstSegmentWidth;
  
  var segments = firstSegmentWidth == 0 ? [] : [beforeDecimal.substr(0, firstSegmentWidth)];
  
  for (var i=0; i < segmentCount; i++) {
    segments.push(beforeDecimal.substr(firstSegmentWidth + (i * 3), 3));
  };
  
  beforeDecimal = symbol + segments.join(",");
  
  return afterDecimal ? String.format("{0}.{1}", beforeDecimal, afterDecimal) : beforeDecimal;
};

/**
 * Old versions.  Not sure if we should be augmenting String like the above so have left this for reference
 */

// /**
//  * @param {String} str A string to be capitalized
//  * @returns A capitalized string (e.g. "some test sentence" becomes "Some test sentence")
//  * @type String
//  */
// String.capitalize = function(str) {
//   return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
// };
// 
// /**
//  * @param {String} str A string to be turned into title case
//  * @returns The string in Title Case (e.g. "some test sentence" becomes "Some Test Sentence")
//  * @type String
//  */
// String.titleize = function(str) {
//   return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
// };
// 
// /**
//  * Takes any string and de-underscores and uppercases it
//  * e.g. long_underscored_string => LongUnderscoredString
//  */
// String.camelize = function(class_name_string) {
//   return String.titleize(class_name_string.replace(/_/g, " ")).replace(/ /g, "");
// 
//   // this feels nicer, sadly no collect function (yet) though
//   // class_name_string.split("_").collect(function(e) {return String.capitalize(e)}).join("");
// };

ExtMVC.Router = function() {};
ExtMVC.Router.prototype = {
  
  /**
   * @property mappings
   * @type Array
   * Readonly. Maintains the collection of defined routes
   */
  mappings: [],
  
  /**
   * @property namedRoutes
   * @type Object
   * Readonly. Maintains the collection of named routes
   */
  namedRoutes: {},
  
  /**
   * Adds a new route to the collection.  Routes are given priority in the order they are added
   * @param {String} re The regular expression-style string to match (e.g. ":controller/:action/:id")
   * @param {Object} additional_params Any additional options which will be returned along with the match elements if the route matches
   * @return {ExtMVC.Route} The newly created route object
   */
  connect: function(re, additional_params) {
    var route = new ExtMVC.Route(re, additional_params);
    this.mappings.push(route);
    
    return route;
  },
  
  /**
   * Defines a named route.  This is the same as using connect, but with the option to specify the route by name.  e.g.:
   * this.name('myRoute', 'my/custom/route/:id', {controller: 'myController', action: 'myAction'});
   * this.urlFor('myRoute', {id: 100}); //returns 'my/custom/route/100'
   * @param {String} routeName The string name to give this route
   * @param {String} re The regular expression-style string to match (e.g. ":controller/:action/:id")
   * @param {Object} additional_params Any additional options which will be returned along with the match elements if the route matches
   */
  name: function(routeName, re, additional_params) {
    this.namedRoutes[routeName] = this.connect(re, additional_params);
  },
  
  /**
   * Same as calling connect("", options) - connects the empty route string to a controller/action pair
   * @params {Object} options An object containing at least a controller and optionally an action (which is otherwise defaulted to index)
   */
  root: function(options) {
    var options = options || {};
    this.connect("", Ext.applyIf(options, { action: 'index' }));
  },
  
  /**
   * Adds specific index, new, show and edit routes for this resource. e.g.:
   * resources('videos') is equivalent to:
   * name('videos_path',     'videos',          {controller: 'videos', action: 'index'});
   * name('new_video_path',  'videos/new',      {controller: 'videos', action: 'new'  });
   * name('video_path',      'videos/:id',      {controller: 'videos', action: 'show' });
   * name('edit_video_path', 'videos/:id/edit', {controller: 'videos', action: 'edit' });
   *
   * You can pass a second parameter which is an options object, e.g.:
   * resources('videos', {controller: 'myController', myKey: 'myValue'}) sets up the following:
   * name('videos_path',     'videos',          {controller: 'myController', myKey: 'myValue', action: 'index'});
   * name('new_video_path',  'videos/new',      {controller: 'myController', myKey: 'myValue', action: 'new'  });
   * name('video_path',      'videos/:id',      {controller: 'myController', myKey: 'myValue', action: 'show' });
   * name('edit_video_path', 'videos/:id/edit', {controller: 'myController', myKey: 'myValue', action: 'edit' });
   * 
   * Also accepts a series of arguments - resources('videos', 'bookmarks', 'messages')
   * is the same as calling resources with each
   *
   * Finally, this format is also accepted:
   * resources('videos', 'bookmarks', 'messages', {controller: 'myController', myKey: 'myValue'})
   * Which is equivalent to calling resources with each of the three strings in turn, each with the
   * final argument passed as options
   */
  resources: function(resource_name, options) {
    //if we have been passed a bunch of strings, call resources with each
    if (arguments[1] && typeof(arguments[1]) == 'string') {
      var lastArg = arguments[arguments.length - 1];
      
      var opts = (typeof(lastArg) == 'object') ? lastArg : {};
      
      for (var i=0; i < arguments.length; i++) {
        //don't call with the last argument if it is an object as this is a generic settings object
        if (!(lastArg === arguments[i] && typeof(lastArg) == 'object')) {
          this.resources(arguments[i], opts);
        };
      };
      
      return;
    };
    
    //work out the named route names for index, show, new and edit actions
    var indexName = String.format("{0}_path",      resource_name.pluralize()  );
    var newName   = String.format("new_{0}_path",  resource_name.singularize());
    var showName  = String.format("{0}_path",      resource_name.singularize());
    var editName  = String.format("edit_{0}_path", resource_name.singularize());
    
    //add named routes for index, new, edit and show
    this.name(indexName, resource_name,               Ext.apply({}, {controller: resource_name, action: 'index'}));
    this.name(newName,   resource_name + '/new',      Ext.apply({}, {controller: resource_name, action: 'new'  }));
    this.name(showName,  resource_name + '/:id',      Ext.apply({}, {controller: resource_name, action: 'show', conditions: {':id': "[0-9]+"}}));
    this.name(editName,  resource_name + '/:id/edit', Ext.apply({}, {controller: resource_name, action: 'edit', conditions: {':id': "[0-9]+"}}));
  },
  
  /**
   * Given a hash containing at route segment options (e.g. {controller: 'index', action: 'welcome'}),
   * attempts to generate a url and redirect to it using Ext.History.add.
   * All arguments are passed through to this.urlFor()
   * @param {Object} options An object containing url segment options (such as controller and action)
   * @return {Boolean} True if a url was generated and redirected to
   */
  redirectTo: function() {
    var url = this.urlFor.apply(this, arguments);
    if (url) {
      Ext.History.add(url);
      return true;
    } else return false;
  },
  
  /**
   * Constructs and returns a config object for a Ext.History based link to a given url spec.  This does not create
   * an Ext.Component, only a shortcut to its config.  This is intended for use in quickly generating menu items
   * @param {Object} urlOptions A standard url generation object, e.g. {controller: 'index', action: 'welcome'}
   * @param {Object} linkOptions Options for the link itself, e.g. {text: 'My Link Text'}
   * @return {Object} a constructed config object for the given parameters
   */
  linkTo: function(urlOptions, linkOptions) {
    var linkOptions = linkOptions || {};
    
    var url = this.urlFor(urlOptions);
    if (url) {
      return Ext.applyIf(linkOptions, {
        url:     url,
        cls:     [urlOptions.controller, urlOptions.action, urlOptions.id].join("-").replace("--", "-").replace(/-$/, ""),
        text:    this.constructDefaultLinkToName(urlOptions, linkOptions),
        handler: function() {Ext.History.add(url);}
      });
    } else throw new Error("No match for that url specification");
  },
  
  /**
   * Attempts to create good link name for a given object containing action and controller.  Override with your own 
   * function to create custom link names for your app
   * @param {Object} urlOptions An object containing at least controller and action properties
   * @param {Object} linkOptions An object of arbitrary options for the link, initially passed to linkTo.  Not used
   * in default implementation but could be useful when overriding this method
   * @return {String} The best-guess link name for the given params.
   */
  constructDefaultLinkToName: function(urlOptions, linkOptions) {
    if (!urlOptions || !urlOptions.controller || !urlOptions.action) {return "";}
    var linkOptions = linkOptions || {};
    
    Ext.applyIf(linkOptions, {
      singularName: urlOptions.controller.singularize()
    });
    
    var actionName = urlOptions.action.titleize();
    if (actionName == 'Index') {
      return "Show " + urlOptions.controller.titleize();
    } else {
      return actionName + " " + linkOptions.singularName.titleize();
    }
  },
  
  /**
   * @params {String} url The url to be matched by the Router.  Router will match against
   * all connected matchers in the order they were connected and return an object created
   * by parsing the url with the first matching matcher as defined using the connect() method
   * @returns {Object} Object of all matches to this url
   */
  recognise: function(url) {
    for (var i=0; i < this.mappings.length; i++) {
      var m = this.mappings[i];
      var match = m.matchesFor(url);
      if (match) { return match; }
    };
    
    return false;
  },
  
  /**
   * Takes an object of url generation options such as controller and action.  Returns a generated url.
   * For a url to be generated, all of these options must match the requirements of at least one route definition
   * @param {Object/String} options An object containing url options, or a named route string.
   * @param {Object} namedRouteOptions If using a named route, this object is passed as additional parameters.  e.g:
   * this.name('myRoute', 'my/custom/route/:id', {controller: 'myController', action: 'myAction'});
   * this.urlFor('myRoute', {id: 100}); //returns 'my/custom/route/100'
   * this.urlFor('myRoute', 100);       //returns 'my/custom/route/100' - number argument assumed to be an ID
   * this.urlFor('myRoute', modelObj);  //returns 'my/custom/route/100' if modelObj is a model object where modelObj.data.id = 100
   * @return {String} The generated url, or false if there was no match
   */
  urlFor: function(options, namedRouteOptions) {
    var route;
    
    //named route
    if (typeof(options) == 'string') {
      if (route = this.namedRoutes[options]) {
        var namedRouteOptions = namedRouteOptions || {};
        
        //normalise named route options in case we're passed an integer
        if (typeof(namedRouteOptions) == 'number') {
          namedRouteOptions = {id: namedRouteOptions};
        };
        
        //normalise named route options in case we're passed a model instance
        if (namedRouteOptions.data && namedRouteOptions.data.id) {
          namedRouteOptions = {id: namedRouteOptions.data.id};
        };
        
        return route.urlForNamed(namedRouteOptions);
      };
    } 
    //non-named route
    else {
      for (var i=0; i < this.mappings.length; i++) {
        route = this.mappings[i]; var u = route.urlFor(options);
        if (u) { return u; }
      };
    };
    
    //there were no matches so return false
    return false;
  },
  
  //experimental...
  withOptions: function(options, routes) {
    var options = options || {};
    
    var defaultScope = this;
    var optionScope  = {};
    
    optionScope.prototype = this;
    Ext.apply(optionScope, {
      connect: function(re, additional_params) {
        var additional_params = additional_params || {};
        Ext.applyIf(additional_params, options);
        
        defaultScope.connect.call(defaultScope, re, additional_params);
      },
      
      name: function(routeName, re, additional_params) {
        var additional_params = additional_params || {};
        Ext.applyIf(additional_params, options);
        
        defaultScope.name.call(defaultScope, routeName, re, additional_params);
      }
    });
    
    routes.call(this, optionScope);
  }
};

/**
 * Basic default routes.  Redefine this method inside config/routes.js
 */
ExtMVC.Router.defineRoutes = function(map) {
  map.connect(":controller/:action");
  map.connect(":controller/:action/:id");
};

/**
 * 
 * 
 */
ExtMVC.Route = function(mappingString, options) {
  this.mappingString = mappingString;
  this.options       = options || {};
  
  //The regular expression we use to match a segment of a route mapping
  //this will recognise segments starting with a colon,
  //e.g. on 'namespace/:controller/:action', :controller and :action will be recognised
  this.paramMatchingRegex = new RegExp(/:([0-9A-Za-z\_]*)/g);
  
  /**
   * Converts a route string into an array of symbols starting with a colon. e.g.
   * ":controller/:action/:id" => [':controller', ':action', ':id']
   */
  this.paramsInMatchString = this.mappingString.match(this.paramMatchingRegex) || [];
  this.paramsInStringWithOptions = [];
  
  /**
   * Store and remove any route conditions specified
   */
  this.conditions = this.options.conditions || {};
  if (this.options.conditions) { delete this.options.conditions; }
  
  for (var i=0; i < this.paramsInMatchString.length; i++) {
    this.paramsInStringWithOptions.push(this.paramsInMatchString[i]);
  };
  
  for (var o in options) {
    this.paramsInStringWithOptions.push(":" + o);
  }
  
  this.matcherRegex = this.convertToUsableRegex(mappingString);
};

ExtMVC.Route.prototype = {
  /**
   * @param {url} The url we want to match against this route to see if it matches
   * @return {boolean} Returns true if this route matches the url
   */
  recognises: function(url) {
    return this.matcherRegex.test(url);
  },
  
  /**
   * @param {url} The url we want to provide matches for
   * @return {Object} Object of all matches for this url, as well as additional params as defined in the route
   */
  matchesFor: function(url) {
    if (!this.recognises(url)) {return false;};
    
    var parameters = {};
    
    var keys   = this.paramsInMatchString;
    var values = url.match(this.matcherRegex);
    values.shift(); //first value is the entire match so reject
    
    for (var i = keys.length - 1; i >= 0; i--){
      parameters[keys[i].replace(":", "")] = values[i];
    };
    
    //add any additional parameter options specified in the route definition
    for (option in this.options) {
      parameters[option] = this.options[option];
    }
    
    return parameters;
  },
  
  urlForNamed: function(options) {
    var options = options || {};
    
    return this.urlFor(Ext.applyIf(options, this.options));
  },
  
  /**
   * Attempts to build a url with this route, swapping the placeholders with properties of the options hash
   */
  urlFor: function(options) {
    var url = this.mappingString;
    
    for (var o in options) {
      //values in options must match this.options - e.g. this.options.action must be the same as options.action
      if (options[o] && this.options[o] && options[o] != this.options[o]) { return false; }
    }
    
    
    //TODO: Tidy this up.  All of it

    var paramsInOptions = [];
    for (var o in options) {
      paramsInOptions.push(":" + o);
    }
    
    paramsInOptions = paramsInOptions.sort();
    var paramsInStringWithOptions = this.paramsInStringWithOptions.sort();
    
    //make sure that all match elements in the url string are included.  If not, return false
    if (paramsInStringWithOptions.length != paramsInOptions.length) { return false; }
    for (var i=0; i < paramsInOptions.length; i++) {
      if (paramsInOptions[i] != paramsInStringWithOptions[i]) {
        return false;
      }
    };
    
    for (var o in options) {
      url = url.replace(":" + o, options[o]);
    }
    
    return url;
  },
  
  /**
   * Private: For a given string, replaces all substrings starting with a colon into
   * a regex string that can match a url segment. 
   * e.g. :controller/:action => '^([a-zA-Z0-9\_]+)/([a-zA-Z0-9\_]+)$'
   * If any conditions have been specified on this route, their regular expressions are used instead:
   * :controller/:action/:id => '^([a-zA-Z0-9\_]+)/([a-zA-Z0-9\_]+)/([0-9]+)$'
   * if conditions was set to {":id" =>/[0-9]+]/}
   * @param {String} regex_string The string we want to turn into a matchable regex
   * @return {String} The replaced string
   */
  convertToUsableRegex: function(regex_string) {
    var p = this.paramsInMatchString;
    
    for (var i = p.length - 1; i >= 0; i--){
      var cond = this.conditions[p[i]];
      var matcher = String.format("({0})", cond || "[a-zA-Z0-9\_,]+");
      
      regex_string = regex_string.replace(new RegExp(p[i]), matcher);
    };
    
    //we want to match the whole string, so include the anchors
    return new RegExp("^" + regex_string + "$");
  }
};

/**
 * ExtMVC.Controller
 * @extends Ext.util.Observable
 * Controller base class
 */
ExtMVC.Controller = function(config) {
  var config = config || {};
  Ext.applyIf(config, {
    autoRegisterViews: true
  });
  
  ExtMVC.Controller.superclass.constructor.call(this, config);
  
  /**
   * @property os
   * @type ExtMVC.OS
   * Maintains a reference to the current OS
   */
  //we need to wrap this in try/catch because OS also inherits from Controller, so can't call getOS()
  //get.  Hmm
  try {
    this.os = ExtMVC.OS.getOS();
  } catch(e) {};
  
  /**
   * @property views
   * @type Object
   * Hash of all views registered for use with this Controller.
   * Read only, use @link {getViewClass} for access
   */
  this.views = {};
  
  /**
   * @property runningViews
   * @type Object
   * Hash of all views which are currently rendered and referenced.  Indexed by the view's ID
   * Read only!  Use renderView and destroyView to add or remove.
   */
  this.runningViews = {};
  
  /**
   * @property actions
   * @type Object
   * Maintains a reference to each registered action. Read only.
   */
  this.actions = {};
  
  this.addEvents(
    /**
     * @event init
     * Fires when this controller is created.
     * @param {ExtMVC.Controller} this The controller instance
     */
    'init',
    
    /**
     * @event beforedefaultaction
     * Fires is this.fireAction('someAction') is called, but 'someAction' has not been defined
     * with this.registerAction().  Default behaviour in this case is to attempt to find a view
     * of the same name and display it.  If you want to provide your own behaviour here, just
     * return false to this event
     * @param {string} actionName The name of the action being fired
     * @param {Object} scope The scope the action is being fired within
     * @param {Array} args An array of arguments passed to the action
     */
    'beforedefaultaction'
  );
  
  /**
   * Automatically register all views in the config.viewsPackage
   */
  if (config.autoRegisterViews && config.viewsPackage) {
    for (var v in config.viewsPackage) {
      this.registerView(v.toLowerCase(), config.viewsPackage[v]);
    }
  };
  
  this.fireEvent('init', this);
};

Ext.extend(ExtMVC.Controller, Ext.util.Observable, {
  
  /**
   * Attaches a view class to this controller - allows the controller to create the view
   * @param {String} viewName The name of this view (e.g. 'index')
   * @param {Function} viewClass A reference to the view class to be instantiated (e.g. MyApp.view.Index, which is some subclass of Ext.Panel)
   */
  registerView: function(viewName, viewClass) {
    this.views[viewName] = viewClass;
  },
  
  /**
   * Convenience method to batch register views with this Controller
   * @param {Object} An object of viewName: viewClass options.  e.g. {'index': MyApp.view.Index}
   * would be the same as this.registerView('index', MyApp.view.Index)
   */
  registerViews: function(viewObject) {
    for (var v in viewObject) {
      this.registerView(v, viewObject[v]);
    }
  },
  
  /**
   * Returns the view class registered for the given view name, or null
   * @param {String} viewName The name registered for this view with this controller
   * @return {Function/null} The view class (or null if not present)
   */
  getViewClass: function(viewName) {
    return this.views[viewName];
  },
  
  /**
   * @property renderMethod
   * @type String
   * Can be either renderNow or return.  renderNow attempts to actually render the view, whereas return just returns it
   */
  renderMethod: 'renderNow',
  
  /**
   * @property addTo
   * @type Ext.Container
   * The container to add views to using the 'add' renderMethod.  Usually set to an Ext.TabPanel instance or similar
   */
  addTo: null,
  
  /**
   * @property model
   * @type Function/Null
   * Defaults to null.  If set to a reference to an ExtMVC.Model subclass, renderView will attempt to dynamically
   * scaffold any missing views, if the corresponding view is defined in the ExtMVC.view.scaffold package
   */
  model: null,
  
  /**
   * Returns a reference to the Scaffold view class for a given viewName
   * @param {String} viewName The name of the view to return a class for (index, new, edit or show)
   * @return {Function} A reference to the view class to instantiate to render this scaffold view
   */
  scaffoldViewName: function(viewName) {
    return ExtMVC.view.scaffold[viewName.titleize()];
  },
  
  /**
   * Creates the requested view and registers it with this.runningViews.
   * Each view should have a unique ID, or the view is not rendered and an error is raised
   * @param {String} viewName The registered name of the view to create and render
   * @param {Object} viewConfig Generic config object passed to the view's constructor
   * @param {Object} renderConfig Render configuration options
   * @return {Object} A reference to the newly created view
   * @throws Error when a view for the given viewName has not been registered, or if the view's ID has already been taken
   */
  renderView: function(viewName, viewConfig, renderConfig) {
    var renderConfig = renderConfig || {};
    var viewConfig   = viewConfig   || {};
    
    //by default we render straight away, to the Body element
    Ext.applyIf(renderConfig, {
      renderTo:   Ext.getBody(),
      renderNow:  true,
      renderOnce: true
    });
    
    var v;
    
    //if a view class has been registered
    if (typeof((this.getViewClass(viewName))) == 'function') {
      v = new (this.getViewClass(viewName))(viewConfig);
      
    //view has not been registered, attempt to create one
    } else if (this.model) {
      try {
        v = new (this.scaffoldViewName(viewName))(this.model);
      } catch(e) {};
    }
    
    if (!v || typeof(v) == 'undefined') {return;}
    
    var running = this.getRunningView(v.id);
    if (running) {
      if (renderConfig.renderOnce) {
        v.destroy();
        return running;
      } else {
        //view ID has already been taken, throw an error
        throw new Error('The view ID ' + v.id + ' has already been taken.  Each view instance must have a unique ID');        
      };
    } else {
      return this.launchView(v);
    };

  },
  
  /**
   * Launches a view instance by either rendering it to the renderTo element or adding it to this.addTo
   * @param {Ext.Component} v Any Component which can be added to a container or rendered to an element
   * @param {Object} renderConfig A config object with instructions on how to render this view.  Relevant options are:
   *                              renderNow: true to render the view right away (defaults to true)
   *                              renderTo: The element to render this view to (unless using the addTo method)
   */
  launchView: function(v, renderConfig) {
    var renderConfig = renderConfig || {};
    Ext.applyIf(renderConfig, {
      renderNow: true,
      renderTo:  Ext.getBody()
    });
    
    v.on('close',   function()     {this.destroyView(v.id); },            this);
    v.on('destroy', function(view) {delete this.runningViews[view.id]; }, this);
    this.runningViews[v.id] = v;
    
    if (this.renderMethod == 'renderNow' && renderConfig.renderNow) {
      v.render(renderConfig.renderTo, renderConfig.renderPosition);
      return v;
    } else {
      if (this.addTo && renderConfig.renderNow) {
        this.addTo.add(v).show();
        this.addTo.doLayout();
        return v;
      };
      
      // XXX
      // TODO there needs to be some kind of exception here if nothing happens
      return v;
    };
  },
  
  /**
   * Returns an instance of a running view with the given ID
   * @param {String} viewId The unique ID of a view instantiation
   * @return {Object} The view instance, or null if no match
   */
  getRunningView: function(viewId) {
    return this.runningViews[viewId];
  },
  
  /**
   * Returns an array of all running view instances whose IDs match the given regex
   * @param {Object} regex The regular expression to compare unique view instance IDs to
   * @return {Array} An array of matching view instances
   */
  getRunningViews: function(regex) {
    //make sure we have a regex object
    if (!regex.test) { return []; }
    
    var matches = [];
    for (var v in this.runningViews) {
      if (regex.test(v)) {
        matches.push(this.runningViews[v]);
      };
    }
    
    return matches;
  },
  
  /**
   * Destroys the currently running view as referenced by the argument.
   * The argument must be an <em>ID</em> of a running view, not the registered viewName.
   * Check that a running view for a given ID exists using this.getRunningView.
   * @param {String} viewId The ID of the running view to destroy
   */
  destroyView: function(viewId) {
    var view = this.getRunningView(viewId);
    if (view) {
      view.destroy();
      delete this.runningViews[viewId];
    };
  },
  
  /**
   * Destroys all views with IDs matching the given regex
   * @param {Object} regex The Regular Expression to match IDs against
   */
  destroyViews: function(regex) {
    //make sure we have a regex object
    if (!regex.test) { return []; }
    
    for (var v in this.runningViews) {
      if (regex.test(v)) {
        this.destroyView(v);
      };
    }
  },
  
  /**
   * Registers a function as an Action for this Controller.  Actions registered here
   * can take part in this.fireAction, and are automatically given before and after events.
   * For example registerAction('index', function() { ... }) registeres before_index and after_index
   * events, which are called before and after the action has been fired.  Return false to the before_
   * event in order to stop the action being called.
   * @param {String} actionName The string name to associate with this action function
   * @param {Function} actionFunction The action function to be called with this.fireAction(actionName)
   * @param {Object} options Gives ability to optionally bypass creation of before_ and after_ events
   * (e.g. {before_filter: false, after_filter: true}).  Both default to true
   * Also takes an overwrite option which will stop this action overwriting a previous action defined with
   * this action name (defaults to true)
   */
  registerAction: function(actionName, actionFunction, options) {
    var options = options || {};
    Ext.applyIf(options, { before_filter: true, after_filter: true, overwrite: true});
    
    //create the before and after filters
    if (options.before_filter) { this.addEvents('before_' + actionName); }
    if (options.after_filter)  { this.addEvents('after_'  + actionName); }
    
    //don't overwrite the existing action if told not to
    if (!this.getAction(actionName) || options.overwrite == true) {
      this.actions[actionName] = actionFunction;
    };
  },
  
  /**
   * Returns a reference to the function of the requested action name.  Does not execute the action
   * @param {String} actionName The string name of the action to find
   * @return {Function} A reference to the function, or null if not found
   */
  getAction: function(actionName) {
    return this.actions[actionName];
  },
  
  /**
   * Fires the requested action with any arguments passed to it.  If before and after filters have been
   * added to this action, they are fired before and after the action is executed (see {@link registerAction}).
   * If the specified action has not been registered, this will try to display the registered view with the same name,
   * if one has been defined.
   * @param {String} actionName The name of the action to call.  Nothing will happen if an invalid action name is specified
   * @param {Object} scope The scope in which to execute the action (defaults to the controller's scope).  Set this to null 
   * keep scope as the controller's scope if you need to pass additional params to the action
   * @param {Array} args An array of arguments to execute the action with (optional)
   */
  fireAction: function(actionName, scope, args) {
    var scope = scope || this;
    var args  = args  || [];
    
    var a = this.getAction(actionName);
    if (a) {
      if (this.fireEvent('before_' + actionName)) {
        a.apply(scope, args);
        
        this.fireEvent('after_' + actionName);
      };
    } else {
      if (this.fireEvent('beforedefaultaction', actionName, scope, args)) {
        var v;
        if (v = this.renderView(actionName, args[0])) {
          v.show();
        }
      };
    };
  },
  
  /**
   * Attaches an action to an event on any Ext.util.Observable object. Mainly a convenience method
   * in place of firingObject.on(eventName, actionName, actionScope)
   * @param {Object} firingObject The object to listen to for fired events
   * @param {String} eventName The name of the event to handle
   * @param {String} actionName The name of the action to dispatch to (this will be called with the arguments attached to the event)
   * @param {Object} actionScope The scope to execute the action within (defaults to this controller)
   */
  handleEvent: function(firingObject, eventName, actionName, actionScope) {
    var actionScope = actionScope || this;
    
    firingObject.on(eventName, function() {
      this.fireAction(actionName, actionScope, arguments);
    }, this);
  }
});

Ext.reg('controller', ExtMVC.Controller); 

/**
 * This started life as a plugin, moving it here for now as it might be useful on every project (hence oddish definition)
 */


Ext.ns('ExtMVC.plugin.CrudController');

(function() {
  var c = ExtMVC.plugin.CrudController;
  
  /**
   * Adds default CRUD (Create, Read, Update, Delete) to this controller.
   * This is currently injected into Controller's prototype, so a simple call to this.actsAsCrudController()
   * with the following params sets everything up. It adds the following actions:
   * 
   * new() - by default displays the view called 'new' in this controller's view namespace
   * edit() - displays the 'edit' view and loads the model whose id is in your os.params.id
   * create(form) - takes an Ext.form.BasicForm and attempts to create + save a new model object with it
   * update(form) - takes a BasicForm and attempts to update an existing object with it (again using os.params.id)
   * destroy(id, store) - takes an ID, deletes it and optionally refreshes a store
   * 
   * It does not overwrite any existing action already defined under one of those names.
   *
   * Override any of the default methods like this:
   * 
   * this.actsAsCrudController, MyNamespace.models.MyModel, {
   *   onUpdateFailure: function() {... your custom onUpdateFailure behaviour ...}
   * });
   *
   * @param {ExtMVC.Model} model The model to provide CRUD support for
   * @return {ExtMVC.Controller} The controller, now with addition actions and methods
   */
  c.registerActions = function(model, overrides) {
    Ext.apply(this, overrides, c.defaultFunctions);
    
    this.addEvents(
      /**
       * @event findsuccess
       * Fires after a successful load has taken place (applies to Edit forms)
       * @param {ExtMVC.Model} modelObj The instantiated model object found by the lookup
       */
      'findsuccess',
      
      /**
       * @event findfailure
       * Fires if the model instance could not be found
       */
      'findfailure'
    );
    
    /**
     * @property model
     * @type ExtMVC.Model
     * Holds a reference to the model this controller provides CRUD support for
     */
    this.model = model;
    
    if (!this.model) {
      throw new Error("You must provide a model to this.actsAsCrudController().  " +
                      "Pass it as the first argument to actsAsCrudController or set " + 
                      "'this.model = YourModel' before calling actsAsCrudController.");
    };
    
    /**
     * @action new 
     * Renders the new form for this model
     */
    this.registerAction('new', function() {
      this.form = this.renderView('new');
    }, {overwrite: false});
    
    /**
     * @action edit 
     * Renders the edit form and loads the model data into it
     */
    this.registerAction('edit', function() {
      this.form = this.renderView('edit');
      this.form.el.mask('Loading...', 'x-mask-loading');
      
      this.loadForm(this.form);
    }, {overwrite: false});
    
    /**
     * @action create 
     * Creates a new model instance, displays errors if required and redirects to index
     */
    this.registerAction('create', function(form) {
      form.el.mask('Saving...', 'x-mask-loading');
      this.onCreate(form);
    }, {overwrite: false});
    
    /**
     * @action update
     * Updates an existing model instance, displays errors if required and redirects to index
     */
    this.registerAction('update', function(form) {
      form.el.mask('Saving...', 'x-mask-loading');
      this.onUpdate(form);
    }, {overwrite: false});
    
    /**
     * @action destroy 
     * Deletes a given model instance
     */
    this.registerAction('destroy', function(id, store) {
      var id = id || this.os.params.id;
      if (id) {
        var u = new this.model({id: id});
        u.destroy({
          scope:   this,
          success: this.onDestroySuccess.createDelegate(this, [store]),
          failure: this.onDestroyFailure
        });
      };
    }, {overwrite: false});
  };
  
  c.defaultFunctions = {
    
    /**
     * @property modelObj
     * @type ExtMVC.Model/Null
     * Reference to the model being edited in this form.  Is set once loaded by the adapter
     */
    modelObj: null,
    
    /**
     * @property loadUrl
     * @type String/Null
     * If your form needs to load from a non-standard url, override this (should be very rare).
     * Defaults to null, which lets the model choose which url to load from
     */
    loadUrl: null,
    
    /**
     * @property saveUrl
     * @type String
     * If your form needs to save to a non-standard url, override this (should be very rare).
     * Defaults to null, which lets the model choose which url to save to
     */
    saveUrl: null,
  
    /**
     * Loads the form with model data
     * @param {Ext.form.FormPanel} form a reference to the form into which to load the data
     */
    loadForm: function(form) {
      this.model.findById(this.os.params.id, {
        scope:    this,
        url:      this.loadUrl,
        success:  this.onFindSuccess,
        failure:  this.onFindFailure,
        callback: form.el.unmask.createDelegate(form.el)
      });
    },
    
    /**
     * Fires after successful find of the model.  Loads data into the form
     * @param {ExtMVC.Model} modelObj The found model object
     */
    onFindSuccess: function(modelObj) {
      this.editModelObj = modelObj;
      this.form.getForm().loadRecord(modelObj);
      
      this.fireEvent('findsuccess', modelObj);
    },
    
    /**
     * Fires if the model object could not be loaded for whatever reason.
     * By default it offers the user to try again or go back
     */
    onFindFailure: function() {
      this.fireEvent('findfailure');
      Ext.Msg.show({
        title:   'Load Failed',
        msg:     'The item could not be loaded',
        buttons: {yes: 'Try again', no: 'Back'},
        scope:   this,
        fn:      function(btn) { btn == 'yes' ? this.loadForm() : Ext.History.back(); }
      });
    },
    
    /**
     * Called when the save button on an edit form is pressed.  By default this will load the model object with the form values,
     * then call save() on the model object.  Override onCreateSuccess and onCreateFailure to update success and failure callbacks
     * @param {Ext.form.BasicForm} form A reference to the FormPanel
     */
    onCreate: function(form) {
      this.newModelObj = new this.model({});
      
      this.onSave(form, this.newModelObj, {
        success:  this.onCreateSuccess,
        failure:  this.onCreateFailure
      });
    },
    
    /**
     * Called when the save button on an edit form is pressed.  By default this will load the model object with the form values,
     * then call save() on the model object.  Override onUpdateSuccess and onUpdateFailure to update success and failure callbacks
     * @param {Ext.form.BasicForm} form A reference to the FormPanel
     */
    onUpdate: function(form) {
      this.onSave(form, this.editModelObj, {
        success:  this.onUpdateSuccess,
        failure:  this.onUpdateFailure
      });
    },
    
    /**
     * Used by both onCreate and onUpdate.  Don't override this unless you understand what you're doing
     */
    onSave: function(form, model, options) {
      //add a saving mask
      form.el.mask('Saving...', 'x-mask-loading');
      
      /**
       * Updates the model (which also saves it).  Uses success and failure options passed in from
       * one of onUpdate or onCreate, depending on which one called this
       */
      model.update(form.getValues(), Ext.apply({}, options, {
        scope:    this,
        url:      this.saveUrl,
        callback: function() {form.el.unmask();}
      }));
    },
    
    /**
     * Called after a successful save.  By default will redirect to the model's index page
     * @param {Object} response The response object from the server
     */
    onSaveSuccess: function(response) {
      this.os.router.redirectTo(Ext.apply({}, { action: 'index' }, this.os.params));
    },
    
    /**
     * Called after successful item creation.  By default this just first onSaveSuccess
     */
    onCreateSuccess: function() {this.onSaveSuccess();},
    
    /**
     * Called after successful item update.  By default this just first onSaveSuccess
     */
    onUpdateSuccess: function() {this.onSaveSuccess();},
    
    /**
     * Called after save fails on create.  By default this will parse server errors and display them on the form
     * @param {Object} response the response object from the server (should be containing errors)
     */
    onCreateFailure: function(modelObj, response) {
      this.addErrorMessages(modelObj, response);
    },
    
    /**
     * Called after save fails on update.  By default this will parse server errors and display them on the form
     * @param {Object} response the response object from the server (should be containing errors)
     */
    onUpdateFailure: function(modelObj, response) {
      this.addErrorMessages(modelObj, response);
    },
    
    /**
     * Adds server errors to the model and form fields. Private.
     * @ignore
     */
    addErrorMessages: function(modelObj, response) {
      this.form.getForm().clearInvalid();
      this.form.getForm().markInvalid(modelObj.errors.forForm());
    },
    
    /**
     * Called after an item has been successfully destroyed (deleted).  By default this reloads the grid's store
     * @param {Ext.data.Store} store The Ext.data.Store to reload after deletion
     */
    onDestroySuccess: function(store) {
      if (store) store.reload();
    },
    
    /**
     * Called after an destroy attempt was made on a model instance, but the attempt failed.  By default this shows
     * a MessageBox alert informing the user
     */
    onDestroyFailure: function(paramName) {
      Ext.Msg.alert(
        'Delete Failed',
        'Sorry, something went wrong when trying to delete that item.  Please try again'
      );
    }
  };
  
  /**
   * Define a method on Controller to enable this.actsAsCrudController(this) within a
   * controller constructor function
   */
  ExtMVC.Controller.prototype.actsAsCrudController = c.registerActions;
  
})();

/**
 * ExtMVC.OS
 * @extends ExtMVC.Controller
 * Specialised ExtMVC.Controller intended to create and manage generic Operating System
 * components (e.g. not elements specific to a single Application within the OS)
 * When an OS is instantiated, ExtMVC.OS.getOS() is defined and returns the OS instance
 */
ExtMVC.OS = function(config) {
  ExtMVC.OS.superclass.constructor.call(this, config);
  
  this.addEvents(
    /**
     * @event beforelaunch
     * Fires before this application launches
     * @param {ExtMVC.Application} this The application about to be launched
     */
    'beforelaunch',
    
    /**
     * @event launch
     * Fires after the application has been launched
     * @param {ExtMVC.Application} this The application which has been launched
     */
    'launch'
  );
  
  this.initialiseNamespaces();
  
  var os = this;
  ExtMVC.OS.getOS = function() {return os;};
};
Ext.extend(ExtMVC.OS, ExtMVC.Controller, {
  /**
   * Registers a controller for use with this OS.  The controller is instantiated lazily
   * when needed, through the use of this.getController('MyController')
   * @param {String} controllerName A string name for this controller, used as a key to reference this controller with this.getController
   * @param {Function} controllerClass A reference to the controller class, which is later instantiated lazily
   */
  registerController: function(controllerName, controllerClass) {
    this.controllers[controllerName] = controllerClass;
  },

  /**
   * Returns a controller instance for the given controller name.
   * Instantiates the controller first if it has not yet been instantiated.
   * @param {String} controllerName The registered name of the controller to get
   * @return {Object} The controller instance, or null if not found
   */
  getController: function(controllerName) {
    var c = this.controllers[controllerName];
    if (c) {
      //instantiate the controller first, if required
      if (typeof c === 'function') {
        this.controllers[controllerName] = new this.controllers[controllerName]();
      };
      return this.controllers[controllerName];
    } else {
      return null;
    };
  },
  
  /**
   * @property controllers
   * When this.registerController('application', MyApp.ApplicationController) is called,
   * the ApplicationController class is registered here under the 'application' key.
   * When this.getController('application') is called, it checks here to see if the 
   * controller has been instantiated yet.  If it has, it is returned.  If not it is
   * instantiated, then returned.
   */
  controllers: {},
  
  /**
   * Launches this application
   */
  launch: function() {
    if (this.fireEvent('beforelaunch', this)) {
      this.initializeRouter();
      this.initializeViewport();
      
      if (this.usesHistory) { this.initialiseHistory(); }      
      
      this.onLaunch();
      this.fireEvent('launch', this);
    };
  },
  
  /**
   * @property usesHistory
   * @type Boolean
   * True to automatically create required DOM elements for Ext.History,
   * sets up a listener on Ext.History's change event to fire this.handleHistoryChange. 
   * False by default
   */
  usesHistory: false,
  
  /**
   * @prop dispatchHistoryOnLoad
   * @type Boolean
   * If usesHistory is true and dispatchHistoryOnLoad is also true, the OS will attempt to match
   * any string currently after the # in the url and dispatch to it
   */
  dispatchHistoryOnLoad: true,
  
  /**
   * @property viewportBuilder
   * @type String
   * The type of viewport to construct (can be any registered with ExtMVC.ViewportBuilderManager)
   */
  viewportBuilder: 'leftmenu',
  
  /**
   * Config object which is passed made available to the ViewportBuilder
   */
  viewportBuilderConfig: {},
  
  /**
   * Called just before onLaunch.  Runs the ExtMVC.ViewportBuilder
   * specified in this.viewportBuilder.
   * Override this to create your own viewport instead of using a builder
   */
  initializeViewport: function() {
    var builder = ExtMVC.ViewportBuilderManager.find(this.viewportBuilder);
    if (builder) {
      builder.build(this);
    };
  },
  
  /**
   * @property params
   * @type Object
   * An object containing the most current parameters (usually decoded from a url using this.router)
   * e.g. {controller: 'index', action: 'welcome', id: 10}
   */
  params: {},
  
  /**
   * Dispatches a request to a registered controller. 
   * @param {Object} dispatchConfig A config object which should look something like this:
   * {controller: 'MyController', action: 'index'}, where 'MyController' is the key for a controller
   * which has been registered to the controller.  If action is not specified, it defaults to 'index'
   * @param {Object} scope The scope in which to fire the event (defaults to the OS)
   * @param {Array} args An array of arguments which are passed to the controller action.
   */
  dispatch: function(dispatchConfig, scope, args) {
    var dispatchConfig = dispatchConfig || {};
    Ext.applyIf(dispatchConfig, {
      action: 'index'
    });
    
    this.params = dispatchConfig;
    
    var c;
    if (c = this.getController(dispatchConfig.controller)) {
      c.fireAction(dispatchConfig.action, scope || c, args || []);
    };
  },
  
  /**
   * Override this to perform custom processing between the beforelaunch and
   * launch events
   */
  onLaunch: Ext.emptyFn,
  
  /**
   * Sets up a Router instance.  This is called automatically before onLaunch()
   * Add routes using this.router.connect
   */
  initializeRouter: function() {
    if (this.router) {return;}
    this.router = new ExtMVC.Router();
    ExtMVC.Router.defineRoutes(this.router);
  },
  
  /**
   * @property name
   * @type String
   * The name to namespace this application under (e.g. 'MyApp').  If set,  the appropriate subnamespaces are created automatically
   */
  name: undefined,
  
  /**
   * Uses Ext.namespace to create packages view controllers, models and views
   * E.g. if name = 'Blog' or this.name = 'Blog', this is the same as:
   * Ext.ns('Blog', 'Blog.controllers', 'Blog.models', 'Blog.views')
   */
  initialiseNamespaces: function(name) {
    var name = name || this.name;
    if (name) {
      Ext.ns(name, name + '.controllers', name + '.models', name + '.views');
    };
  },
  
  /**
   * Creates the necessary DOM elements required for Ext.History to manage state
   * Sets up listeners on Ext.History's change event to fire the dispatch() action in the normal way.
   * This is not called automatically as not all applications will need it
   */
  initialiseHistory: function() {
    this.historyForm = Ext.getBody().createChild({
      tag:    'form',
      action: '#',
      cls:    'x-hidden',
      id:     'history-form',
      children: [
        {
          tag: 'div',
          children: [
            {
              tag:  'input',
              id:   'x-history-field',
              type: 'hidden'
            },
            {
              tag: 'iframe',
              id:  'x-history-frame'
            }
          ]
        }
      ]
    });
    
    //initialize History management.  Fire a dispatch event if a hash url is present and startup
    //dispatch is requested
    if (this.dispatchHistoryOnLoad) {
      Ext.History.init(function(history) {
        var hash   = document.location.hash.replace("#", "");
        var params = this.router.recognise(hash);
        if (params) {this.dispatch(params);}
      }, this);
    } else {
      Ext.History.init();
    }
    
    Ext.History.on('change', this.handleHistoryChange, this);
  },
  
  /**
   * Takes a history token (anything after the # in the url), consults the router and dispatches
   * to the appropriate controller and action if a match was found
   * @param {String} token The url token (e.g. the token would be cont/act/id for a url like mydomain.com/#cont/act/id)
   */
  handleHistoryChange: function(token) {
    var match = this.router.recognise(token);
    
    if (match) {
      this.dispatch(match, null, [{url: token}]);
    };
  },
  
  /**
   * Simple convenience function to change the document title whenever this view is displayed
   * @param {Ext.Component} view The view (Ext.Panel subclass normally) to listen to show/activate events on
   * @param {String} title The string to change the document title to (defaults to view.initialConfig.title)
   */
  setsTitle: function(view, title) {
    var title = title || view.title || view.initialConfig ? view.initialConfig.title : null;
    if (title) {
      view.on('show',     function() {document.title = title;});
      view.on('activate', function() {document.title = title;});
    };
  }
});

Ext.reg('os', ExtMVC.OS);

// ExtMVC.getOS = ExtMVC.OS.getOS();

/**
 * ExtMVC.Model
 * @extends Ext.util.Observable
 * Base model class
 */
ExtMVC.Model = function(fields, config) {
  Ext.applyIf(this, {
    /**
     * @property newRecord
     * @type Boolean
     * True if this record is newly created and has not yet been successfully saved
     */
    newRecord: fields.id ? false : true
  });
  
  //create a new Record object and then decorate it with RecordExtensions
  var record = ExtMVC.Model.recordFor(this.modelName, fields);
  var rec = new record(fields || {});
  rec.init(this);
  
  Ext.applyIf(this, this.constructor.instanceMethods);
  
  //add any hasMany associations
  var hm = this.constructor.hasMany;
  if (hm) {
    //make sure we're dealing with an array
    if (typeof hm == 'string') { hm = [hm]; }
    
    for (var i=0; i < hm.length; i++) {
      var hma = hm[i];
      
      //association can just be specified via a string, in which case turn it into an object here
      if (typeof hma == 'string') { hma = { name: hma }; };
      
      hma = new ExtMVC.Model.HasManyAssociation(this, hma);
      this[hma.associationName] = hma;
    };
  };
  
  //add any belongsTo associations
  var bt = this.constructor.belongsTo;
  if (bt) {
    //make sure we're dealing with an array
    if (typeof bt == 'string') { bt = [bt]; }
    
    for (var i=0; i < bt.length; i++) {
      var bta = bt[i];
      
      //association can just be specified via a string, in which case turn it into an object here
      if (typeof bta == 'string') { bta = { name: bta }; };
      
      var btaAssoc = new ExtMVC.Model.BelongsToAssociation(this, bta);
      this[btaAssoc.associationName] = btaAssoc;
      
      //see if a parent has been defined, if not set one up now (defaults here to the first belongsTo assoc)
      var parentModel = this.constructor.parentModel || bta.name;
      if (parentModel && !this.parent) {
        this.parent = btaAssoc;
      };
    };
  };
  
  Ext.apply(this, rec);
};

/**
 * Creates a model definition
 * @param {String} modelNameWithNamespace The (string) name of the model to create (e.g. MyNamespace.model.MyModel)
 * @param {object} config Configuration for this model.
 * @cfg {Array} fields Array of field definitions for this model, same format as Ext.data.Record.create takes
 * @cfg {String} adapter The data adapter to use (defaults to REST, which attempts to save models to a RESTful url backend)
 * @cfg {String} urlName The string version of the model name to use when making requests to the server.  e.g. for a model called
 * MyModel the server may be set up to accept urls like /my_models/1 or /MyModels/1, this is where you specify that
 * @cfg {String} xmlName The name of the XML element which contains the model fields.  e.g. for a model called MyModel, this may look
 * like <MyModel>...</MyModel> or <my_model>...</my_model>.  This is where you set that (don't include the angle brackets)
 */
ExtMVC.Model.define = function(modelNameWithNamespace, config) {
  var config = config || {};
  
  //split into namespace and model name
  var nsRegex = /(.+)\.([A-Za-z]*)$/;
  var match = nsRegex.exec(modelNameWithNamespace);
  var namespace = null;
  if (match) {
    var namespace = match[1];
    var modelName = match[2];
    Ext.ns(namespace); //make sure the namespace is defined
  };

  Ext.applyIf(config, {
    namespace: namespace, //.split(".")[0],
    modelName: modelName,
    className: modelName,
    adapter:   'REST'
  });
  
  //extend ExtMVC.Model for this className
  eval(modelNameWithNamespace + " = Ext.extend(ExtMVC.Model, config)");
  var className = eval(modelNameWithNamespace);
  
  /**
   * If we are extending another model, copy its fields, class methods and instance methods
   * into this model
   */
  if (className.prototype.extend) {
    var extendsModel = eval(className.prototype.extend);
    var parentFields = extendsModel.fields;
    
    //add parent model fields to the front of the child model fields array
    for (var i = parentFields.length - 1; i >= 0; i--){
      var childFields    = className.prototype.fields;
      var alreadyDefined = false;
      
      //check that this field is not redefined in the child model
      for (var j=0; j < childFields.length; j++) {
        if (childFields[j].name == parentFields[i].name) {
          alreadyDefined = true;
          break; //no need to finish the loop as we've already made the match
        }
      };
      
      //only add if not redefined in child model
      if (!alreadyDefined) {
        className.prototype.fields.unshift(parentFields[i]);
      };
    };
    
    //add any class methods
    Ext.applyIf(className, extendsModel.prototype);
  };
  
  /**
   * Add fields the way Ext.data.Record does it.
   * TODO: We shouldn't be doing this here, Record should be doing it... not very DRY
   */
  className.prototype.fields = new Ext.util.MixedCollection();
  Ext.each(config.fields, function(f) {
    className.prototype.fields.add(f.name, new Ext.data.Field(f));
  });
  
  //add fields, modelName, className and adapter as class-level items
  Ext.apply(className, {
    adapter:   config.adapter,
    modelName: modelName,
    className: className,
    namespace: namespace,
    
    //build the underlying Ext.data.Record now (will be used in model's constructor)
    record:    ExtMVC.Model.recordFor(modelName, config.fields)
  });
  
  //add model class functions such as findById
  ExtMVC.Model.addClassMethodsToModel(className, config);
};


/**
 * Custom extensions to Ext.data.Record.  These methods are added to new Ext.data.Record objects
 * when you subclass ExtMVC.Model.
 * For example
 * model = new ExtMVC.Spec.FakeUser({
 *   id:   100,
 *   name: 'Ed'
 * });
 * alert(model.namespacedUrl('my_url')); // => '/admin/my_url.ext_json'
 */
ExtMVC.Model.RecordExtensions = {
  /**
   * Adds logic on top of Ext.data.Record
   */
  init: function(config) {
    Ext.applyIf(config, {
      //set up the various variations on the model name
      className:         ExtMVC.Model.classifyName(config.modelName),
      controllerName:    ExtMVC.Model.controllerName(config.modelName),
      foreignKeyName:    ExtMVC.Model.foreignKeyName(config.modelName),
      
      humanPluralName:   ExtMVC.Model.pluralizeHumanName(config.modelName),
      humanSingularName: ExtMVC.Model.singularizeHumanName(config.modelName),
      
      underscoreName:    config.modelName
    });
    
    //add the data adapter, initialize it
    var adapter = ExtMVC.Model.AdapterManager.find(config.adapter || ExtMVC.Model.prototype.adapter);
    if (adapter) {
      Ext.apply(config, adapter.instanceMethods);
      adapter.initialize(this);
    }
    
    //mix in validations package
    Ext.apply(config, ExtMVC.Model.ValidationExtensions);
    config.initializeValidationExtensions();
    
    Ext.apply(this, config);
  },
  
  /**
   * Calculates a nested url for this object based on it's data.id and parent model
   * @return {String} The url for this model object
   */
  url: function() {
    var el = this.data.id ? this : this.constructor;
    if (this.parent && this.parent.lastFetched) {
      return ExtMVC.UrlBuilder.urlFor(this.parent.get({}, -1), el);
    } else {
      return ExtMVC.UrlBuilder.urlFor(el);
    };
  },
  
  /**
   * Mass-assigns field values.  Operation is wrapped in beginEdit and endEdit
   * e.g. setValues({first_name: 'Ed', last_name: 'Spencer'})
   * is the same as set('first_name', 'Ed'); set('last_name': 'Spencer')
   * @param {Object} values An object containing key: value pairs for fields on this object
   */
  setValues: function(values) {
    this.beginEdit();
    for (var key in values) {
      this.set(key, values[key]);
    }
    this.endEdit();
  },
  
  /**
   * Reads errors from a generic object and adds them to this model's internal errors object.
   * Intended to be used mainly to process server responses
   */
  readErrors: function(errorsObject) {
    this.errors.readServerErrors(errorsObject);
  }
};

/**
 * Provides a framework for validating the contents of each field
 */
ExtMVC.Model.ValidationExtensions = {
  /**
   * Sets up this record's validation parameters
   */
  initializeValidationExtensions: function() {
    this.validations = this.validations || [];
    this.errors      = new ExtMVC.Model.Validation.Errors(this);
  },
  
  isValid: function() {
    return this.errors.isValid();
  }
};


ExtMVC.Model.models   = [];

/**
 * Utility methods which don't need to be declared per model
 */
Ext.apply(ExtMVC.Model, {
  
  /**
   * Retrieves or creates an Ext.data.Record for the given model name.  This is then cached
   * in ExtMVC.models for later reuse
   * @param {String} modelName The name of the model to create or retrieve a record for
   * @param {Array} fields An array of fields to be passed to the Ext.data.Record.create call
   * @return {Ext.data.Record} An instantiated Record object using Ext.data.Record.create
   */
  recordFor: function(modelName, fields) {
    var record = ExtMVC.Model.models[modelName];
    if (!record) {
      record = Ext.data.Record.create(fields);

      Ext.apply(record.prototype, ExtMVC.Model.RecordExtensions);
      ExtMVC.Model.models[modelName] = record;
    }
    
    return record;
  },
    
  /**
   * String methods:
   */
   
  urlizeName : function(name) {
    return name.toLowerCase().pluralize();
  },
  
  classifyName: function(name) {
    return this.singularizeHumanName(name).replace(/ /g, "");
  },
  
  singularizeHumanName: function(name) {
    return name.replace(/_/g, " ").titleize();
  },
  
  pluralizeHumanName: function(name) {
    return name.pluralize().replace(/_/g, " ").titleize();
  },
  
  controllerName : function(name) {
    return this.pluralizeHumanName(name).replace(/ /g, "")  + "Controller";
  },
  
  foreignKeyName: function(name) {
    return name.toLowerCase() + '_id';
  },
  
  /**
   * Add class methods for finding model objects
   * @param {Function} modelClass The class to add methods to
   * @param {Object} additionalFunctions (Optional) extra class methods to add to this class
   */
  addClassMethodsToModel: function(modelClass, additionalFunctions) {
    var additionalFunctions = additionalFunctions || {};
    
    Ext.applyIf(additionalFunctions, {
      //add a urlName property to the Model subclass
      urlName: ExtMVC.Model.urlizeName(modelClass.prototype.modelName)
    });
    
    //add any class methods from the adapter
    var adapter = ExtMVC.Model.AdapterManager.find(modelClass.adapter || ExtMVC.Model.prototype.adapter);
    if (adapter && adapter.classMethods) {
      Ext.apply(modelClass, adapter.classMethods);
    };
        
    //add other class methods    
    Ext.apply(modelClass, {      
      /**
       * Returns the default reader for this model subclass.  Creates a default reader if
       * one has not already been set
       */
      getReader: function() {
        if (!modelClass.reader) {
          modelClass.reader = new Ext.data.JsonReader({
            totalProperty: 'totalCount',
            root: modelClass.jsonName || modelClass.prototype.modelName.toLowerCase()
          }, modelClass);
        };
        
        return modelClass.reader;
      }
    }, additionalFunctions);
  }
});

Ext.ns('ExtMVC.Model.Adapter', 'ExtMVC.Model.Validation');

/**
 * Manages registration and retrieval of MVC Model adapters
 * @class ExtMVC.Model.AdapterManager
 */
ExtMVC.Model.AdapterManager = {
  /**
   * @property adapters
   * @type Object
   * Key/Value pairs of registered names and the relevant Adapter objects
   */
  adapters: {},
  
  /**
   * Registers an adapter for use with MVC Models.  
   * @param {String} name String name for this Adapter (e.g. 'REST')
   * @param {Function} object A reference to the Adapter object itself
   */
  register: function(name, constructor) {
    this.adapters[name] = constructor;
  },
  
  /**
   * Retrieves the requested adapter by key name
   * @param {String} name The name of the adapter to fine (e.g. 'REST')
   * @return {Object/Null} The Adapter object, if found
   */
  find: function(name, config) {
    return this.adapters[name];
  }
};

/**
 * @class ExtMVC.Model.Cache
 * @extends Ext.util.Observable
 * Provides an interface for caching model objects which have been fetched from some database/backend
 */
ExtMVC.Model.Cache = function(config) {
  var config = config || {};
 
  ExtMVC.Model.Cache.superclass.constructor.call(this, config);
  
  this.addEvents(
    /**
     * @event beforeadd
     * Fires before an item is added to the cache
     * @param {ExtMVC.Model} modelObject The model which is about to be added
     */
    'beforeadd',
    
    /**
     * @event add
     * Fires after an item is added to the cache
     * @param {ExtMVC.Model} modelObject The model which was just added
     */
    'add',
    
    /**
     * @event beforeclear
     * Fires before the cache is cleared
     * @param {Number} seconds The number of seconds worth of caches which will be saved
     */
    'beforeclear',
    
    /**
     * @event clear
     * Fires after the cache has been cleared
     * @param {Number} seconds The number of seconds worth of caches which were saved
     */
    'clear'
  );
};

Ext.extend(ExtMVC.Model.Cache, Ext.util.Observable, {
  
  /**
   * @property caches
   * @type Object
   * Maintains all cached objects
   */
  caches: {},
  
  /**
   * Adds the given model object to the cache.  Automatically stores the datetime of the add
   * @param {ExtMVC.Model} modelObject The model you want to store in the cache
   */
  add: function(modelObject) {
    if (this.fireEvent('beforeadd', modelObject)) {
      var modelName = modelObject.className;
      var modelId   = modelObject.data.id;

      if (modelName && modelId) {
        modelObject.cachedAt = new Date();
        
        this.caches[modelName] = this.caches[modelName] || {};        
        this.caches[modelName][modelId] = modelObject;
        
        this.fireEvent('add', modelObject);
        return true;
      } else {
        
        return false;
      };
    }
  },
  
  /**
   * Fetches an object from the cache
   * @param {Object} params params object which must contain at least modelName and id.  Optionally 
   * supply staleTime, which is the number of seconds old the cached object is allowed to be to get a hit,
   * or a Date which will restrict hits to anything cached after that date
   * @return {ExtMVC.Model/null} The model if found, or null
   */
  fetch: function(params) {
    this.caches[params['modelName']] = this.caches[params['modelName']] || {};
    
    var params = params || {};
    var hit    = this.caches[params['modelName']][params['id']];
    
    if (hit) {
      if (params.staleTime) {
        if (typeof params.staleTime == 'number') {
          var date = new Date();
          date.setTime(date.getTime() - (1000 * params.staleTime));
          params.staleTime = date;
        };
        
        //make sure we have a date object
        if (params.staleTime.getTime && hit.cachedAt > params.staleTime) {
          return hit;
        }
      } else {
        return hit;
      };
    };
  },
  
  /**
   * Clears all objects more than the given number of seconds old (defaults to clearing all objects)
   * @param {Number} seconds The number of seconds to keep cached objects for (e.g. setting this to 10 would delete anything cached more than 10 seconds ago)
   */
  clear: function(seconds) {
    var seconds = seconds || 0;
    var date = new Date();
    date.setTime(date.getTime() - (1000 * seconds));
    
    if (this.fireEvent('beforeclear', seconds)) {
      if (seconds == 0) {
        this.caches = {};
      } else {
        for (var i=0; i < this.caches.length; i++) {
          for (var j=0; j < this.caches[i].length; j++) {
            if (this.caches[i][j].cachedAt < date) {
              delete this.caches[i][j];
            };
          };
        };
      };
      
      this.fireEvent('clear', seconds);
    }
  }
});

/**
 * @class ExtMVC.UrlBuilder
 * Builds URLs...
 */
ExtMVC.UrlBuilder = function() {
  
};

ExtMVC.UrlBuilder.prototype = {
  
  /**
   * @property baseUrlNamespace
   * @type String
   * An optional url namespace to prepend to all urls (e.g. /admin).  Defaults to an empty string
   */
  baseUrlNamespace: '',
  
  /**
   * @property baseUrlFormat
   * @type String
   * An optional url extension to append to all urls (e.g. .json).  Defaults to an empty string
   */
  baseUrlFormat: '',
  
  /**
   * @property segmentJoiner
   * @type String
   * The string to join url segments on (defaults to '/')
   */
  segmentJoiner: '/',
  
  /**
   * Generates a url for the given object or function definition
   * @param {Mixed} obj The object to create a URL for
   * @return {String} The generated URL
   */
  urlFor: function() {
    var config  = {};
    var lastArg = Array.prototype.slice.call(arguments, arguments.length - 1)[0];
    
    //if the last argument is a config object
    if (typeof lastArg == 'object' && !lastArg.className) {
      //set some default url building options
      Ext.apply(config, lastArg, {
        format:       this.baseUrlFormat,
        urlNamespace: this.baseUrlNamespace
      });
      
      //use all but the last argument now
      var arguments = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
    };
    
    //set some default url building options
    Ext.applyIf(config, {
      format:       this.baseUrlFormat,
      urlNamespace: this.baseUrlNamespace
    });
    
    var segments = [config.urlNamespace];
    
    //iterate over valid arguments, appending each to segments
    for (var i=0; i < arguments.length; i++) {
      var arg = arguments[i];
      var res = [];
      
      switch(typeof arg) {
        case 'string':   res = [arg];                         break;
        case 'object':   res = this.segmentsForInstance(arg); break;
        case 'function': res = this.segmentsForClass(arg);    break;
      }
      
      for (var j=0; j < res.length; j++) {
        segments.push(res[j]);
      };
    };
    
    var url = segments.join(this.segmentJoiner);
    if (config.format) { url += "." + config.format; }
    
    return url;
  },
  
  /**
   * Returns an array of url segments for a model instance
   * @param {ExtMVC.Model} instance A Model instance with at least its primary key value set
   * @return {Array} An array of segments for this url (e.g. ['users', '10'])
   */
  segmentsForInstance: function(instance) {
    return [instance.constructor.urlName, instance.data.id];
  },
  
  /**
   * Returns an array of url segments for a given class and optional primary key
   * @param {Function} cls The class to generate a url for
   * @param {String} id An optional ID to add to the segments
   * @return {Array} An array of segments for this class
   */
  segmentsForClass: function(cls, id) {
    var segs = [cls.urlName];
    if (id) { segs.push(id); }
    
    return segs;
  }
};

ExtMVC.UrlBuilder = new ExtMVC.UrlBuilder();

ExtMVC.Model.Association = {
  
  /**
   * Returns the default association name for a given class (e.g. "Post" becomes "posts", "SecretAgent" becomes "secretAgents" etc)
   * @param {String} className The string name of the class which this belongs to
   * @return {String} The association name for this class
   */
  hasManyAssociationName: function(className) {
    return className.toLowerCase() + 's';
  },
  
  /**
   * Returns the default association name for a given class (e.g. "Post" becomes "post", "SecretAgent" becomes "secretAgent")
   * @param {String} className The string name of the class to calculate a belongsTo name for
   * @return {String} The association name for this class
   */
  belongsToAssociationName: function(className) {
    return className.toLowerCase();
  }
};



// //this is not currently used
// ExtMVC.Model.Association = function(ownerObject, config) {
//   var config = config || {};
//   
//   //set some sensible default values
//   Ext.applyIf(config, {
//     primaryKey:      'id',
//     foreignKey:      ownerObject.foreignKeyName,
//     extend:          {},
//     className:       (ownerObject.constructor.namespace ? ownerObject.constructor.namespace + '.' + config.name : config.name)
//   });
//   
//   // //get a reference to the class definition function of the associated object
//   // //(e.g. a hasMany: ['Post'] association will return a reference to Post)
//   // var associatedObjectClass = eval(config.className);
//   // 
//   // /**
//   //  * Private, calls the ownerObject's class method with the supplied args
//   //  */
//   // function callOwnerObjectClassMethod(method, args, scope) {
//   //   return ownerObject.constructor[method].apply(scope || ownerObject.constructor, args || []);
//   // };
//   // 
//   // /**
//   //  * Private, calls the associated object's class method with the supplied args
//   //  */
//   // function callAssociatedObjectClassMethod (method, args, scope) {
//   //   return associatedObjectClass[method].apply(scope || associatedObjectClass, args || []);
//   // }
// };

/**
 * @class ExtMVC.Model.HasManyAssociation
 * @extends ExtMVC.Model.Association
 */
ExtMVC.Model.HasManyAssociation = function(ownerObject, config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    offset:          0,
    limit:           25,
    associationName: ExtMVC.Model.Association.hasManyAssociationName(config.name)
  });

  //TODO: these should be abstracted to a parent object (as should private vars and funcs below)
  Ext.applyIf(config, {
    primaryKey:      'id',
    foreignKey:      ownerObject.foreignKeyName,
    extend:          {},
    
    className:       (ownerObject.constructor.namespace ? ownerObject.constructor.namespace + '.' + config.name : config.name)
  });
  
  //get a reference to the class definition function of the associated object
  //(e.g. a hasMany: ['Post'] association will return a reference to Post)
  var associatedObjectClass = eval(config.className);
  
  /**
   * Private, calls the ownerObject's class method with the supplied args
   */
  function callOwnerObjectClassMethod(method, args, scope) {
    return ownerObject.constructor[method].apply(scope || ownerObject.constructor, args || []);
  };
  
  /**
   * Private, calls the associated object's class method with the supplied args
   */
  function callAssociatedObjectClassMethod (method, args, scope) {
    return associatedObjectClass[method].apply(scope || associatedObjectClass, args || []);
  }
  
  return Ext.applyIf(config.extend, {
    
    /**
     * @property associationName
     * @type String
     * Returns the name of this association so that the model can add it to its definition
     */
    associationName: config.associationName,
    
    /**
     * @property associationType
     * @type String
     * The type of association (hasMany or belongsTo)
     */
    associationType: 'hasMany',
    
    /**
     * Returns the URL for this association - e.g. if model User hasMany Posts, user.posts.url() will
     * return something like /users/1/posts.
     * Pass additional parameter options as arguments to pass straight through to the URL builder - e.g.:
     * user.posts.url('published'); // = /users/1/posts/published
     * @return {String} The URL for the has many resource
     */
    url: function() {
      var args = [ownerObject, associatedObjectClass];
      for (var i=0; i < arguments.length; i++) {
        args.push(arguments[i]);
      };
      
      return ExtMVC.UrlBuilder.urlFor.apply(ExtMVC.UrlBuilder, args);
    },
    
    /**
     * Passes through to the owner class's findById class method, adding a foreign key constraint first
     * @param {String} id The ID of the associated object to retrieve
     * @param {Object} options Options passed along to the associated model's Class's findById method.  Pass in loadSuccess, loadFailure, conditions, order etc here
     */
    findById: function(id, options) {
      var options = options || {};
      
      //add a condition to constrain to the owner object's id
      if (!options.conditions) { options.conditions = []; }
      options.conditions.push({
        key:   config.foreignKey,
        value: ownerObject.data[config.primaryKey]
      });
      
      return callAssociatedObjectClassMethod('findById', [id, options]);
    },
    
    findAll: function(storeOptions) {
      var storeOptions = storeOptions || {};
      Ext.applyIf(storeOptions, {
        url: this.url(),
        
        listeners: {
          
          //Once we have fetched all hasMany records, make sure newRecord is false, and set the parent
          //relationship to point to this ownerObject (The object which hasMany of these records)
          'load': {
            scope: this,
            fn: function(store, records, options) {
              Ext.each(records, function(record) {
                record.newRecord = false;
                if (record.parent && record.parent.set) {
                  record.parent.set(ownerObject);
                }
              }, this);
            }
          }
        }
      });
      
      return callAssociatedObjectClassMethod('findAll', [storeOptions]);
    },
    
    /**
     * Creates (builds and attempts to save) this associated model
     * @param {Object} fields Object with keys and values to initialise this object
     * @param {Object} saveConfig Passed to the Ext.Ajax request, supply success and failure options here
     */
    create: function(fields, saveConfig) {
      return this.build(fields).save(saveConfig);
    },
    
    /**
     * Builds an instantiation of the associated model with the supplied data.
     * Automatically links in the correct foreign key
     * @param {Object} fields The data to initialize this object with
     */
    build: function(fields) {
      var fields = fields || {};
      
      //instantiate the new object with the augmented fields
      var obj = new associatedObjectClass(fields);
      
      //set up the object's belongsTo association.  This also sets up the foreign key
      var assocName = ExtMVC.Model.Association.belongsToAssociationName(ownerObject.className);
      obj[assocName].set(ownerObject);
      
      return obj;
    },

    /**
     * Adds an existing (saved) instantiation of the associated model to this model's hasMany collection
     * @param {ExtMVC.Model} modelObject The existing, saved model
     */
    add: function(modelObject) {
      //TODO: implement this
      
    },

    destroy: function(id) {
      //TODO: implement this
      
    }
  });
};

// Ext.extend(ExtMVC.Model.HasManyAssociation, ExtMVC.Model.Association);

/**
 * @class ExtMVC.Model.BelongsToAssociation
 * @extends ExtMVC.Model.Association
 */
ExtMVC.Model.BelongsToAssociation = function(ownerObject, config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    associationName: ExtMVC.Model.Association.belongsToAssociationName(config.name)
  });
    
  //TODO: these should be abstracted to a parent object (as should private vars and funcs below)
  Ext.applyIf(config, {
    primaryKey:      'id',
    foreignKey:      ownerObject.foreignKeyName,
    extend:          {},
    
    className:       (ownerObject.constructor.namespace ? ownerObject.constructor.namespace + '.' + config.name : config.name)
  });
  
  //get a reference to the class definition function of the associated object
  //(e.g. a hasMany: ['Post'] association will return a reference to Post)
  var associatedObjectClass = eval(config.className);
  
  /**
   * Private, calls the ownerObject's class method with the supplied args
   */
  function callOwnerObjectClassMethod(method, args, scope) {
    return ownerObject.constructor[method].apply(scope || ownerObject.constructor, args || []);
  };
  
  /**
   * Private, calls the associated object's class method with the supplied args
   */
  function callAssociatedObjectClassMethod (method, args, scope) {
    return associatedObjectClass[method].apply(scope || associatedObjectClass, args || []);
  };
  
  return {
    /**
     * @property associationName
     * @type String
     * Returns the name of this association so that the model can add it to its definition
     */
    associationName: config.associationName,
    
    /**
     * @property associationClass
     * @type ExtMVC.Model
     * A reference to the association's class (e.g. belongsTo: "Post" would have associationClass of Post)
     */
    associationClass: associatedObjectClass,
    
    /**
     * @property associationType
     * @type String
     * The type of association (hasMany or belongsTo)
     */
    associationType: 'belongsTo',
    
    /**
     * @property lastFetched
     * @type Date
     * Date object representing the last time the associated object was successfully fetched
     */
    lastFetched: null,
    
    /**
     * Sets the associated model for this association to the specified model object
     * @param {ExtMVC.Model} modelObject The associated model to set this belongsTo association to
     */
    set: function(modelObject) {
      this.lastFetched = new Date();
      this.cachedObject = modelObject;
      
      //add the foreign key automatically
      ownerObject.data[modelObject.foreignKeyName] = modelObject.data[config.primaryKey];
    },
    
    /**
     * Gets the associated model for this association
     * @param {Object} options Options to pass through to the Ajax load request
     * @param {Number} cacheFor If the object has been retrieved less than this number of seconds ago, use the cached object
     */
    get: function(options, cacheFor) {
      var options  = options  || {};
      var cacheFor = cacheFor || 0;
      
      Ext.applyIf(options, {
        loadSuccess: Ext.emptyFn,
        loadFailure: Ext.emptyFn
      });
      
      var cacheIsCurrent = (((new Date() - this.lastFetched) / 1000) < cacheFor) || (cacheFor == -1);
      
      if (this.lastFetched && this.cachedObject && cacheIsCurrent) {
        //return the cached object via a callback
        options.loadSuccess.call(options.scope || this, this.cachedObject);
        
        //also return via normal return if this is a cached object.  This allows some functions to use the cached object
        //without the overhead of setting up a callback, so long as they first check that the object has been fetched
        return this.cachedObject;
      } else {
        //inject caching code before loadSuccess - caches the object into this.cachedObject and sets this.lastFetched to now
        Ext.apply(options, {
          loadSuccess: options.loadSuccess.createInterceptor(function(obj) {
            this.cachedObject = obj;
            this.lastFetched  = new Date();
          }, this)
        });
        
        return callAssociatedObjectClassMethod('findById', [1, options]);
      };
    }
  };
};

ExtMVC.Model.Adapter.Abstract = {
  initialize: function(model) {
    
  },
  
  classMethods: {
    find: function(options) {
      
    },
    
    findById: function(id, options) {
      return this.findByField('id', id, options);
    },
    
    findByField: function(fieldName, matcher, options) {
      
    },
    
    findAll: function(options) {
      
    }
  },
  
  instanceMethods: {
    save:    Ext.emptyFn,
    
    reload:  Ext.emptyFn,
    
    destroy: Ext.emptyFn
  }
};

/**
 * Methods adding url data mapping
 */
ExtMVC.Model.AbstractAdapter = {
  /**
   * Set up the model for use with Active Resource.  Add various url-related properties to the model
   */
  initAdapter: function() {
    Ext.applyIf(this, {
      urlNamespace: '/admin',
      urlExtension: '.ext_json',
      urlName:      ExtMVC.Model.urlizeName(this.modelName)
    });
  },
  
  /**
   * Saves this record.  Performs validations first unless you pass false as the single argument
   */
  save: function(performValidations) {
    var performValidations = performValidations || true;
    
    console.log("saving model");
  },
  
  destroy: function(config) {
    var config = config || {};
    
    console.log("destroying model");
  },
  
  /**
   * Loads this record with data from the given ID
   * @param {Number} id The unique ID of the record to load the record data with
   * @param {Boolean} asynchronous False to load the record synchronously (defaults to true)
   */
  load: function(id, asynchronous) {
    var asynchronous = asynchronous || true;
    
    console.log("loading model");
  },
  
  /**
   * URL to retrieve a JSON representation of this model from
   */
  singleDataUrl : function(record_or_id) {
    return this.namespacedUrl(String.format("{0}/{1}", this.urlName, this.data.id));
  },
  
  /**
   * URL to retrieve a JSON representation of the collection of this model from
   * This would typically return the first page of results (see {@link #collectionStore})
   */
  collectionDataUrl : function() {
    return this.namespacedUrl(this.urlName);
  },

  /**
   * URL to retrieve a tree representation of this model from (in JSON format)
   * This is used when populating most of the trees in ExtMVC, though
   * only applies to models which can be representated as trees
   */
  treeUrl: function() {
    return this.namespacedUrl(String.format("{0}/tree", this.urlName));
  },
  
  /**
   * URL to post details of a drag/drop reorder operation to.  When reordering a tree
   * for a given model, this url is called immediately after the drag event with the
   * new configuration
   * TODO: Provide more info/an example here
   */
  treeReorderUrl: function() {
    return this.namespacedUrl(String.format("{0}/reorder/{1}", this.urlName, this.data.id));
  },
  
  /**
   * Provides a namespaced url for a generic url segment.  Wraps the segment in this.urlNamespace and this.urlExtension
   * @param {String} url The url to wrap
   * @returns {String} The namespaced URL
   */
  namespacedUrl: function(url) {
    return(String.format("{0}/{1}{2}", this.urlNamespace, url, this.urlExtension));
  }
};

// ExtMVC.Model.registerAdapter('REST', ExtMVC.Model.AbstractAdapter);

Ext.ns('ExtMVC.Model.Adapter');

(function() {
  var A = ExtMVC.Model.Adapter;
  
  A.REST = {
    initialize: function(model) {
      // console.log('initialising REST adapter');
      
      A.Abstract.initialize(model);
    },
    
    classMethods: {
      /**
       * Generic find method, accepts many forms:
       * find(10, opts)      // equivalent to findById(10, opts)
       * find('all', opts)   // equivalent to findAll(opts)
       * find('first', opts) // equivalent to findById(1, opts)
       */
      find: function(what, options) {
        var id;
        if (id = parseInt(what, 10)) {
          return this.findById(id, options);
        };
        
        switch(what) {
          case 'first': return this.findById(1, options);
          default     : return this.findAll(options);
        }
      },
      
      /**
       * Shortcut for findByField('id', 1, {})
       */
      findById: function(id, options) {
        // return this.findByField('id', id, options);
        var options = options || {};
        
        // TODO
        // Old code before below fix
        // Ext.applyIf(options, {
        //   url: this.singleDataUrl(id)
        // });
        
        // This needs to be done as url is set as 'null' in
        // crudcontroller.js line 133.
        // this is temp n00b hack which teh master can fix. can't use apply either.
        if (options.url == null) {
          options.url = this.singleDataUrl(id);
        };
        
        return this.performFindRequest(options);
      },
          
      /**
       * Performs a custom find on a given field and value pair.  e.g.:
       * User.findByField('email', 'adama@bsg.net') creates the following request:
       * GET /users?email=adama@bsg.net
       * And creates an array of User objects based on the server's response
       * @param {String} fieldName The name of the field to search on
       * @param {String/Number} matcher The field value to search for
       * @param {Object} options An object which should contain at least a success function, which will
       * be passed an array of instantiated model objects
       */
      findByField: function(fieldName, matcher, options) {
        var fieldName = fieldName || 'id';
        var options   = options || {};
        
        options.conditions = options.conditions || [];
        options.conditions.push({key: fieldName, value: matcher, comparator: '='});
                
        return this.performFindRequest(options);
      },
      
      findAll: function(options) {
        var options = options || {};
        
        var url = options.url ? options.url : this.collectionDataUrl();
        
        var proxyOpts = {};
        Ext.apply(proxyOpts, this.proxyConfig, {
          url:    url,
          method: "GET"
        });
        
        return new Ext.data.Store(
          Ext.applyIf(options, {
            autoLoad:   true,
            remoteSort: false,
            proxy:      new this.proxyType(proxyOpts),
            reader:     this.getReader()
          })
        );
      },
      
      /**
       * Private, internal methods below here.  Not expected to be useful by anything else but
       * are left public for now just in case
       */
       
      /**
       * Underlying function which handles all find requests.  Private
       */
      performFindRequest: function(options) {
        var options = options || {};
        Ext.applyIf(options, {
          scope:   this,
          url:     this.collectionDataUrl(),
          method:  'GET',
          success: Ext.emptyFn,
          failure: Ext.emptyFn
        });
        
        //keep a handle on user-defined callbacks
        var callbacks = {
          successFn: options.success,
          failureFn: options.failure
        };
        
        // FIXME fix scope issue
        // For some reason the scope isnt correct on this?
        // cant figure out why. scope is set on the applyIf block so it should work..
        var scope = this;
        
        options.success = function(response, opts) {
          scope.parseSingleLoadResponse(response, opts, callbacks);
        };
        
        /**
         * Build params variable from condition options.  Params should always be a string here
         * as we're dealing in GET requests only for a find
         */
        var params = options.params || '';
        if (options.conditions && options.conditions[0]) {
          for (var i=0; i < options.conditions.length; i++) {
            var cond = options.conditions[i];
            params += String.format("{0}{1}{2}", cond['key'], (cond['comparator'] || '='), cond['value']);
          };
          
          delete options.conditions;
        };
        options.params = params;

        return Ext.Ajax.request(options);
      },
      
      /**
       * @property urlExtension
       * @type String
       * Extension appended to the end of all generated urls (e.g. '.js').  Defaults to blank
       */
      urlExtension: '',

      /**
       * @property urlNamespace
       * @type String
       * Default url namespace prepended to all generated urls (e.g. '/admin').  Defaults to blank
       */
      urlNamespace: '',
      
      /**
       * @property port
       * @type Number
       * The web server port to contact (defaults to 80).  Requires host to be set also
       */
      port: 80,
      
      /**
       * @property host
       * @type String
       * The hostname of the server to contact (defaults to an empty string)
       */
      host: "",
      
      /**
       * @property proxyType
       * @type Function
       * A reference to the DataProxy implementation to use for this model (Defaults to Ext.data.HttpProxy)
       */
      proxyType: Ext.data.HttpProxy,
      
      /**
       * @property proxyConfig
       * @type Object
       * Config to pass to the DataProxy when it is created (e.g. use this to set callbackParam on ScriptTagProxy, or similar)
       */
      proxyConfig: {},
      
      /**
       * Called as the 'success' method to any single find operation (e.g. findById).
       * The default implementation will parse the response into a model instance and then fire your own success of failure
       * functions as provided to findById.  You can override this if you need to do anything different here, for example
       * if you are loading via a script tag proxy with a callback containing the response
       * @param {String} response The raw text of the response
       * @param {Object} options The options that were passed to the Ext.Ajax.request
       * @param {Object} callbacks An object containing a success function and a failure function, which should be called as appropriate
       */
      parseSingleLoadResponse: function(response, options, callbacks) {
        var m = this.getReader().read(response);
        if (m && m.records[0]) {
          m.records[0].newRecord = false;
          callbacks.successFn.call(options.scope, m.records[0]);
        } else {
          callbacks.failureFn.call(options.scope, response);
        };
      },
      
      /**
       * URL to retrieve a JSON representation of this model from
       */
      singleDataUrl : function(id) {
        return this.namespacedUrl(String.format("{0}/{1}", this.urlName, id));
      },
  
      /**
       * URL to retrieve a JSON representation of the collection of this model from
       * This would typically return the first page of results (see {@link #collectionStore})
       */
      collectionDataUrl : function() {
        return this.namespacedUrl(this.urlName);
      },
  
      /**
       * URL to retrieve a tree representation of this model from (in JSON format)
       * This is used when populating most of the trees in ExtMVC, though
       * only applies to models which can be representated as trees
       */
      treeUrl: function() {
        return this.namespacedUrl(String.format("{0}/tree", this.urlName));
      },
  
      /**
       * URL to post details of a drag/drop reorder operation to.  When reordering a tree
       * for a given model, this url is called immediately after the drag event with the
       * new configuration
       * TODO: Provide more info/an example here
       */
      treeReorderUrl: function() {
        return this.namespacedUrl(String.format("{0}/reorder/{1}", this.urlName, this.data.id));
      },
  
      /**
       * Provides a namespaced url for a generic url segment.  Wraps the segment in this.urlNamespace and this.urlExtension
       * @param {String} url The url to wrap
       * @returns {String} The namespaced URL
       */
      namespacedUrl: function(url) {
        url = url.replace(/^\//, ""); //remove any leading slashes
        return(String.format("{0}{1}/{2}{3}", this.hostName(), this.urlNamespace, url, this.urlExtension));
      },
      
      /**
       * Builds the hostname if host and optionally port are set
       * @return {String} The host name including port, if different from port 80
       */
      hostName: function() {
        var p = this.port == 80 ? '' : this.port.toString();
        
        if (this.host == "") {
          return "";
        } else {
          return this.port == 80 ? this.host : String.format("{0}:{1}", this.host, this.port);
        };
      }
    },
    
    instanceMethods: {
      /**
       * Saves this model instance to the server.
       * @param {Object} options An object passed through to Ext.Ajax.request.  The success option is a special case,
       * and is called with the newly instantiated model instead of the usual (response, options) signature
       */
      save: function(options) {
        var options = options || {};
        
        if (options.performValidations === true) {
          //TODO: tie in validations here
        };
        
        //keep a reference to this record for use in the success and failure functions below
        var record = this;
        
        //set a _method param to fake a PUT request (used by Rails)
        var params = options.params || this.namespaceFields();
        if (!this.newRecord) { params["_method"] = 'put'; }
        delete options.params;
        
        //if the user passes success and/or failure functions, keep a reference to them to allow us to do some pre-processing
        var userSuccessFunction = options.success || Ext.emptyFn;
        var userFailureFunction = options.failure || Ext.emptyFn;
        delete options.success; delete options.failure;
        
        //function to call if Ext.Ajax.request is successful
        options.success = function(response) {
          //definitely not a new record any more
          record.newRecord = false;
          
          userSuccessFunction.call(options.scope || record, record, response);
        };
        
        //function to call if Ext.Ajax.request fails
        options.failure = function(response) {
          //parse any errors sent back from the server
          record.readErrors(response.responseText);
          
          userFailureFunction.call(options.scope || record, record, response);
        };
        
        //do this here as the scope in the block below is not always going to be 'this'
        var url = this.url();
        
        Ext.applyIf(options, {
          // url:     url, url == null sometimes so this doesnt work
          method:  'POST',
          params:  params
        });
        
        //fix for the above
        if (options.url == null) {
          options.url = url;
        };
        
        Ext.Ajax.request(options);
      },
      
      /**
       * Updates the model instance and saves it.  Use setValues({... new attrs ...}) to change attributes without saving
       * @param {Object} updatedAttributes An object with any updated attributes to apply to this instance
       * @param {Object} saveOptions An object with save options, such as url, callback, success, failure etc.  Passed straight through to save()
       */
      update: function(updatedAttributes, saveOptions) {
        updatedAttributes = updatedAttributes || {};
        saveOptions = saveOptions || {};
        
        this.setValues(updatedAttributes);
        this.save(saveOptions);
      },
      
      reload: function() {
        console.log('reloading');
      },
      
      destroy: function(options) {
        var options = options || {};
        
        Ext.Ajax.request(
          Ext.applyIf(options, {
            url:    this.url(),
            method: 'post',
            params: "_method=delete"
          })
        );
      },
      
      /**
       * Namespaces fields within the modelName string, taking into account mappings.  For example, a model like:
       * 
       * modelName: 'user',
       * fields: [
       *   {name: 'first_name', type: 'string'},
       *   {name: 'last_name',  type: 'string', mapping: 'last'}
       * ]
       * 
       * Will be decoded to an object like:
       * 
       * {
       *   'user[first_name]': //whatever is in this.data.first_name
       *   'user[last]':       //whatever is in this.data.last_name
       * }
       *
       * Note especially that the mapping is used in the key where present.  This is to ensure that mappings work both
       * ways, so in the example above the server is sending a key called last, which we convert into last_name.  When we
       * send data back to the server, we convert last_name back to last.
       */
      namespaceFields: function() {
        var fields    = this.fields;
        var namespace = this.modelName;
        
        var nsfields = {};
        
        for (var i=0; i < fields.length; i++) {
          var item = fields.items[i];
          
          //don't send virtual fields back to the server
          if (item.virtual) {continue;}
          
          nsfields[String.format("{0}[{1}]", namespace.toLowerCase(), item.mapping || item.name)] = this.data[item.name];
        };
        
        //not sure why we ever needed this... 
        // for (f in fields) {
        //   nsfields[String.format("{0}[{1}]", namespace.toLowerCase(), this.data[f.name])] = fields.items[f];
        // }
        
        return nsfields;
      }
    }
  };
})();

ExtMVC.Model.AdapterManager.register('REST', ExtMVC.Model.Adapter.REST);

/**
 * @class ExtMVC.Model.ValidationErrors
 * Simple class to collect validation errors on a model and return them in various formats
 */
ExtMVC.Model.Validation.Errors = function(modelObject) {
  this.modelObject = modelObject;
};

ExtMVC.Model.Validation.Errors.prototype = {
  
  /**
   * @property errors
   * @type Array
   * Raw array of all errors attached to this model.  This is READ ONLY - do not interact with directly
   */
  errors: [],
  
  /**
   * Returns an errors object suitable for applying to a form via BasicForm's markInvalid() method
   * @return {Object} An object with field IDs as keys and formatted error strings as values
   */
  forForm: function() {
    var formErrors = {};
    Ext.each(this.modelObject.fields.items, function(field) {
      var fieldErrors = this.forField(field.name);
      if (fieldErrors.length > 0) {
        formErrors[field.name] = this.joinErrors(fieldErrors);
      };
    }, this);
    
    return formErrors;
  },
  
  /**
   * @property multipleErrorConnector
   * @type String
   * The string to use when connecting more than one error (defaults to 'and')
   */
  multipleErrorConnector: 'and',
  
  /**
   * Joins one or more errors into a human-readable sentence.  For example, there may be two errors on an email field:
   * ["can't be blank", "must be at least 6 characters", "must contain an @"].  This would return:
   * "can't be blank, must be at least 6 characters and must contain an @"
   * @param {Array} errors An array of error messages for a given field
   * @return {String} A human-readable errors sentence
   */
  joinErrors: function(errors) {
    var errors   = errors || [];
    var sentence = "";
    if (errors.length <= 1) { 
      sentence =  errors[0];
    } else {
      //we'll join all but the last error with commas
      var firstErrors = errors.slice(0, errors.length - 1);
      
      //add the last error, with the connector string
      sentence = String.format("{0} {1} {2}", firstErrors.join(", "), this.multipleErrorConnector, errors[errors.length - 1]);
    }
    
    ///add a full stop; sometimes one already present in which case remove final one
    return (/\.$/.test(sentence) ? sentence : sentence + ".").capitalize();
  },
  
  /**
   * Returns an array of all errors for the given field
   * @param {String} field The field to find errors for
   * @return {Array} An array of errors for this field
   */
  forField: function(field) {
    var errs = [];
    
    for (var i=0; i < this.errors.length; i++) {
      var error = this.errors[i];
      if (error[0] == field) { errs.push(error[1]); }
    };
    
    return errs;
  },
  
  /**
   * Returns true if this model currently has no validation errors
   * @return {Boolean} True if this model is currently valid
   */
  isValid: function(paramName) {
    return this.errors.length == 0;
  },
  
  /**
   * Removes all current validation errors
   */
  clearErrors: function() {
    this.errors = [];
  },
  
  /**
   * Parses server response to a failed save, adding each error message to the appropriate field.  Override to provide
   * an implementation for your own server responses.  The default implementation accepts a response like this:
   * {errors: [['some_field', 'some error regarding some_field'], ['another_field', 'another error']]}
   * @param {Object/String} serverErrors A errors object returned by server-side validations.  If this is a string it will
   * @param {Boolean} preserveErrors False to clear all errors before adding errors from server (defaults to false)
   * automatically be turned into an object via Ext.decode
   */
  readServerErrors: function(serverErrors, preserveErrors) {
    var serverErrors = serverErrors || {};
    
    //remove any existing errors unless instructed to preserve them
    if (preserveErrors !== true) {this.clearErrors();}
    
    //make sure we're dealing with an object
    if (typeof(serverErrors) == 'string') {
      serverErrors = Ext.decode(serverErrors);
    };
    
    var rawErrors = serverErrors.errors;
    if (rawErrors) {
      for (var i=0; i < rawErrors.length; i++) {
        this.errors.push(rawErrors[i]);
      };
    };
  }
};

/**
 * A simple manager for registering and retrieving named ViewportBuilders
 * @class ExtMVC.ViewportBuilderManager
 */
ExtMVC.ViewportBuilderManager = {
  
  /**
   * @property viewportBuilders
   * @type Object
   * Key/value pairs for registered viewport builders.  Private
   */
  viewportBuilders: {},
  
  /**
   * Registers a ViewportBuilder with the manager
   * @param {String} name String name for this ViewportBuilder (e.g. 'desktop')
   * @param {Function} constructor A reference to the constructor of the ViewportBuilder
   */
  register: function(name, constructor) {
    this.viewportBuilders[name] = constructor;
  },
  
  find: function(name, config) {
    var c = this.viewportBuilders[name];
    if (c) {
      return new c(config);
    };
  }
};

ExtMVC.ViewportBuilder = function(config) {
  this.initialConfig = config;
};

ExtMVC.ViewportBuilder.prototype = {
  
  /**
   * Abstract function which should be overridden by your implementation
   * @param {ExtMVC.OS} os A reference to the OS.  Usually a builder would set 
   * os.viewport = new Ext.Viewport({...}) and return the os at the end of the function
   * @return {ExtMVC.OS} The operating system as passed in parameters after viewport is built
   */
  build: Ext.emptyFn
};

/**
 * @class ExtMVC.view.scaffold.ScaffoldFormPanel
 * @extends Ext.form.FormPanel
 * Base class for any scaffold form panel (e.g. new and edit forms)
 */
ExtMVC.view.scaffold.ScaffoldFormPanel = Ext.extend(Ext.form.FormPanel, {
  
  /**
   * Sets up the FormPanel, adds default configuration and items
   */
  constructor: function(model, config) {
    var config = config || {};
    
    this.model = model;
    this.os    = ExtMVC.OS.getOS();
    
    this.controllerName = this.model.modelName.pluralize();
    this.controller     = this.os.getController(this.controllerName);
    
    ExtMVC.view.scaffold.ScaffoldFormPanel.superclass.constructor.call(this, config);
  },
  
  /**
   * Adds default items, keys and buttons to the form
   */
  initComponent: function() {
    Ext.applyIf(this, {
      autoScroll: true,
      items:      this.buildItems(this.model),
      keys: [
        {
          key:     Ext.EventObject.ESC,
          scope:   this,
          handler: this.onCancel
        },
        {
          key:       's',
          ctrl:      true,
          scope:     this,
          stopEvent: true,
          handler:   this.saveHandler
        }
      ],
      buttons: [
        {
          text:    'Save',
          scope:   this,
          iconCls: 'save',
          handler: this.saveHandler
        },
        {
          text:    'Cancel',
          scope:   this,
          iconCls: 'cancel',
          handler: this.onCancel
        }
      ]
    });
    
    //sets the document's title to the title of this panel
    this.os.setsTitle(this);
    
    ExtMVC.view.scaffold.ScaffoldFormPanel.superclass.initComponent.apply(this, arguments);
  },
  
  /**
   * @property formItemConfig
   * @type Object
   * Default config which will be passed to all form items
   */
  formItemConfig: {
    anchor: '-40',
    xtype:  'textfield'
  },
  
  /**
   * @property ignoreFields
   * @type Array
   * An array of fields not to show in the form (defaults to empty)
   */
  ignoreFields: ['id', 'created_at', 'updated_at'],
  
  /**
   * Builds an array of form items for the given model
   * @param {ExtMVC.Model} model The model to build form items for
   * @return {Array} An array of auto-generated form items
   */
  buildItems: function(model) {
    //check to see if FormFields have been created for this model
    //e.g. for a MyApp.models.User model, checks for existence of MyApp.views.users.FormFields
    var formFields;
    
    if (formFields = eval(String.format("{0}.views.{1}.FormFields", model.namespace.split(".")[0], model.modelName.pluralize().toLowerCase()))) {
      return formFields;
    };
    
    //no user defined form fields, generate them automatically
    var items = [];
    
    for (var i=0; i < model.fields.length; i++) {
      var f = model.fields[i];
      
      //add if it's not a field to be ignored
      if (this.ignoreFields.indexOf(f.name) == -1) {
        items.push(Ext.applyIf({
          name:        f.name,
          fieldLabel: (f.name.replace(/_/g, " ")).capitalize()
        }, this.formItemConfig));
      };
    };
    
    return items;
  },
  
  /**
   * Called when the save button is clicked or CTRL + s pressed.  By default this simply fires
   * the associated controller's 'update' action, passing this.getForm() as the sole argument
   */
  onCreate: function() {
    this.controller.fireAction('create', null, [this.getForm()]);
  },
  
  onUpdate: function() {
    this.controller.fireAction('update', null, [this.getForm()]);
  },
  
  /**
   * Called when the cancel button is clicked or ESC pressed.  By default this simply calls Ext.History.back
   */
  onCancel: Ext.History.back
});

Ext.reg('scaffold_form_panel', ExtMVC.view.scaffold.ScaffoldFormPanel);

/**
 * @class ExtMVC.view.scaffold.Index
 * @extends Ext.grid.GridPanel
 * A default index view for a scaffold (a paging grid with double-click to edit)
 */
ExtMVC.view.scaffold.Index = function(model, config) {
  var config = config || {};
  
  this.model = model;
  this.os    = ExtMVC.OS.getOS();
  
  this.controllerName = model.modelName.pluralize();
  this.controller     = this.os.getController(this.controllerName);
  
  //we can't put these in applyIf block below as the functions are executed immediately
  config.columns = config.columns || this.buildColumns(model);
  config.store   = config.store   || model.findAll();
  
  var tbarConfig = this.hasTopToolbar    ? this.buildTopToolbar()                : null;
  var bbar       = this.hasBottomToolbar ? this.buildBottomToolbar(config.store) : null;
  
  Ext.applyIf(config, {
    title:      'Showing ' + model.prototype.modelName.pluralize().capitalize(),
    viewConfig: { forceFit: true },
    id:         model.prototype.modelName + 's_index',
    
    loadMask: true,
    
    tbar: tbarConfig,
    bbar: bbar,
    
    listeners: {
      'dblclick': {
        scope: this,
        fn: function(e) {
          var obj = this.getSelectionModel().getSelected();
          
          if (obj) {
            this.os.router.redirectTo({controller: this.controllerName, action: 'edit', id: obj.data.id});
          };
        }
      }
    },
    
    keys: [
      {
        key:     'a',
        scope:   this,
        handler: this.onAdd
      },
      {
        key:     'e',
        scope:   this,
        handler: this.onEdit
      },
      {
        key:     Ext.EventObject.DELETE,
        scope:   this,
        handler: this.onDelete
      }
    ]

  });
 
  ExtMVC.view.scaffold.Index.superclass.constructor.call(this, config);
  ExtMVC.OS.getOS().setsTitle(this);
};

Ext.extend(ExtMVC.view.scaffold.Index, Ext.grid.GridPanel, {
  
  /**
   * @property preferredColumns
   * @type Array
   * An array of columns to show first in the grid, if they exist
   * Overwrite ExtMVC.view.scaffold.Index.preferredColumns if required
   */
  preferredColumns: ['id', 'title', 'name', 'first_name', 'last_name', 'login', 'username', 'email', 'email_address', 'content', 'message', 'body'],
  
  /**
   * @property ignoreColumns
   * @type Array
   * An array of columns not to show in the grid (defaults to empty)
   */
  ignoreColumns: ['password', 'password_confirmation'],
  
  /**
   * @property narrowColumns
   * @type Array
   * An array of columns to render at half the average width
   */
  narrowColumns: ['id'],
  
  /**
   * @property wideColumns
   * @type Array
   * An array of columns to render at double the average width
   */
  wideColumns:   ['message', 'content', 'description', 'bio', 'body'],
  
  /**
   * @property hasTopToolbar
   * @type Boolean
   * True to automatically include a default top toolbar (defaults to true)
   */
  hasTopToolbar: true,
  
  /**
   * @property hasBottomToolbar
   * @type Boolean
   * True to automatically include a paging bottom toolbar (defaults to true)
   */
  hasBottomToolbar: true,
    
  /**
   * Takes a model definition and returns a column array to use for a columnModel
   */
  buildColumns: function(model) {
    var columns     = [];
    var wideColumns = [];
    
    //put any preferred columns at the front
    for (var i=0; i < model.fields.length; i++) {
      var f = model.fields[i];
      if (this.preferredColumns.indexOf(f.name) > -1) {
        columns.push(this.buildColumn(f.name));
      }
    };
    
    //add the rest of the columns to the end
    for (var i = model.fields.length - 1; i >= 0; i--){
      var f = model.fields[i];
      //if this field is not in the prefer or ignore list, add it to the columns array
      if (this.preferredColumns.indexOf(f.name) == -1 && this.ignoreColumns.indexOf(f.name) == -1) {
        columns.push(this.buildColumn(f.name));
      };
      
      //if it's been declared as a wide column, add it to the wideColumns array
      if (this.wideColumns.indexOf(f.name)) {
        wideColumns.push(f.name);
      }
    };
    
    //add default widths to each
    var numberOfSegments = columns.length + (2 * wideColumns.length);
    for (var i = columns.length - 1; i >= 0; i--){
      var col = columns[i];
      
      if (this.narrowColumns.indexOf(col.id) > -1) {
        //id col is extra short
        Ext.applyIf(col, { width: 30 });
      } else if(this.wideColumns.indexOf(col.id) > -1) {
        //we have a wide column
        Ext.applyIf(col, { width: 200 });
      } else {
        //we have a normal column
        Ext.applyIf(col, { width: 100 });
      }
    };
    
    return columns;
  },
  
  /**
   * Build a single column object based on a name, adds default properties
   * @param {Object/String} cfg Column config object (can just include a 'name' property).  Also accepts a string, which is translated into the name property
   * @return {Object} A fully-formed column config with default properties set
   */
  buildColumn: function(cfg) {
    var cfg = cfg || {};
    if (typeof(cfg) == 'string') {cfg = {name: cfg};}
    
    return Ext.applyIf(cfg, {
      id:        cfg.name,
      header:    cfg.name.replace(/_/g, " ").titleize(),
      sortable:  true,
      dataIndex: cfg.name
    });
  },
  
  /**
   * Creates Add, Edit and Delete buttons for the top toolbar and sets up listeners to
   * activate/deactivate them as appropriate
   * @return {Array} An array of buttons 
   */
  buildTopToolbar: function() {
    this.addButton = new Ext.Button({
      text:    'New ' + this.model.modelName.titleize(),
      scope:   this,
      iconCls: 'add',
      handler: this.onAdd
    });
    
    this.editButton = new Ext.Button({
      text:     'Edit selected',
      scope:    this,
      iconCls:  'edit',
      disabled: true,
      handler:  this.onEdit
    });
    
    this.deleteButton = new Ext.Button({
      text:     'Delete selected',
      disabled: true,
      scope:    this,
      iconCls:  'delete',
      handler:  this.onDelete
    });
    
    this.getSelectionModel().on('selectionchange', function(selModel) {
      if (selModel.getCount() > 0) {
         this.deleteButton.enable();  this.editButton.enable();
      } else {
        this.deleteButton.disable(); this.editButton.disable();
      };
    }, this);
    
    return [
      this.addButton,  '-',
      this.editButton, '-',
      this.deleteButton
    ];
  },
  
  /**
   * Creates a paging toolbar to be placed at the bottom of this grid
   * @param {Ext.data.Store} store The store to bind to this paging toolbar (should be the same as for the main grid)
   * @return {Ext.PagingToolbar} The Paging Toolbar
   */
  buildBottomToolbar: function(store) {
    //Used for getting human-readable names for this model
    //TODO: this is overkill, shouldn't need to instantiate an object for this...
    var modelObj = new this.model({});
    
    return new Ext.PagingToolbar({
      store:       store,
      displayInfo: true,
      pageSize:    25,
      emptyMsg:    String.format("No {0} to display", modelObj.humanPluralName)
    });
  },
  
  /**
   * Called when the add button is pressed, or when the 'a' key is pressed.  By default this will redirect to the
   * 'New' form for this resource
   */
  onAdd: function() {
    this.os.router.redirectTo({controller: this.controllerName, action: 'new'});
  },
  
  /**
   * Called when the edit button is pressed, or when the 'e' key is pressed.  By default this will look to see if a row
   * is selected, then redirect to the appropriate Edit form.
   * If you override this you'll need to provide the row record lookup yourself
   */
  onEdit: function() {
    var selected = this.getSelectionModel().getSelected();
    if (selected) {
      this.os.router.redirectTo({controller: this.controllerName, action: 'edit', id: selected.data.id});
    }
  },
  
  /**
   * Called when the delete button is pressed, or the delete key is pressed.  By default this will ask the user to confirm,
   * then fire the controller's destroy action with the selected record's data.id and a reference to this grid as arguments.
   */
  onDelete: function() {
    Ext.Msg.confirm(
      'Are you sure?',
      String.format("Are you sure you want to delete this {0}?  This cannot be undone.", this.model.modelName.titleize()),
      function(btn) {
        if (btn == 'yes') {
          var selected = this.getSelectionModel().getSelected();
          if (selected) {
            this.controller.fireAction('destroy', null, [selected.data.id, this.store]);
          }
        };
      },
      this
    );
  }
});

Ext.reg('scaffold_index', ExtMVC.view.scaffold.Index);

/**
 * @class ExtMVC.view.scaffold.New
 * @extends ExtMVC.view.scaffold.ScaffoldFormPanel
 * Shows a generic new form for a given model
 */
ExtMVC.view.scaffold.New = Ext.extend(ExtMVC.view.scaffold.ScaffoldFormPanel, {

  /**
   * Sets this panel's title, if not already set.  Also specifies the save handler to use
   */
  initComponent: function() {
    Ext.applyIf(this, {
      title:       'New ' + this.model.prototype.modelName.capitalize(),
      saveHandler: this.onCreate
    });
    ExtMVC.view.scaffold.New.superclass.initComponent.apply(this, arguments);
  }
});

Ext.reg('scaffold_new', ExtMVC.view.scaffold.New);

/**
 * @class ExtMVC.view.scaffold.Edit
 * @extends ExtMVC.view.scaffold.ScaffoldFormPanel
 * Shows a generic edit form for a given model
 */
ExtMVC.view.scaffold.Edit = Ext.extend(ExtMVC.view.scaffold.ScaffoldFormPanel, {
  
  /**
   * Sets the panel's title, if not already set
   */
  initComponent: function() {
    Ext.applyIf(this, {
      title:       'Edit ' + this.model.prototype.modelName.capitalize(),
      saveHandler: this.onUpdate
    });
    
    ExtMVC.view.scaffold.Edit.superclass.initComponent.apply(this, arguments);
  }
});

Ext.reg('scaffold_edit', ExtMVC.view.scaffold.Edit);

/**
 * @class ExtMVC.view.HasManyEditorGridPanel
 * @extends Ext.grid.EditorGridPanel
 * Provides some sensible defaults for a HasMany editor grid.  For example, given the following models:
 * ExtMVC.Model.define("MyApp.models.User", {
 *   ...
 *   hasMany: "Post"
 * });
 *
 * ExtMVC.Model.define("MyApp.models.Post", {
 *   ...
 *   belongsTo: "User"
 * });
 *
 * Inside the edit User view, if we wanted to be able to quickly edit any of that User's Posts, we can insert
 * a HasManyEditorGridPanel like this:
 *
 * items: [
 *   {
 *     xtype:       'hasmany_editorgrid',
 *     modelObj:    userObj,
 *     association: userObj.posts,
 *     columns:     [... set up editor columns as per a normal EditorGridPanel]
 *   }
 * ]
 *
 * In the example above, userObj refers to the loaded User instance tied to the edit form.  The HasMany editor grid
 * automatically listens to afteredit events and saves the HasMany model (Post in this case).
 */
ExtMVC.view.HasManyEditorGridPanel = Ext.extend(Ext.grid.EditorGridPanel, {
  
  initComponent: function() {
    Ext.applyIf(this, {
      autoScroll: true,
      store:      this.association.findAll(),
      viewConfig: { forceFit: true }
    });
    
    if (this.hasTopToolbar) { this.addTopToolbar(); }
    
    ExtMVC.view.HasManyEditorGridPanel.superclass.initComponent.apply(this, arguments);
    
    /**
     * Set up listening on the afteredit event.  Simply saves the model instance
     */
    this.on('afteredit', function(args) {
      args.record.save({
        success: function() {
          args.record.commit();
        }
      });
    }, this);
    
    /**
     * Set up listening to selection change to activate the Remove button
     */
    this.getSelectionModel().on('selectionchange', function(selModel, selection) {
      if (this.deleteButton) {
        this.deleteButton.enable();
      };
    }, this);
  },
  
  /**
   * @property hasTopToolbar
   * @type Boolean
   * True to automatically show a toolbar at the top of the grid with Add and Delete buttons (defaults to true)
   */
  hasTopToolbar: true,
  
  /**
   * @property hasNewButton
   * @type Boolean
   * True to add a 'New' button to the top toolbar if the top toolbar is present (defaults to true)
   */
  hasNewButton: true,
  
  /**
   * @property hasDeleteButton
   * @type Boolean
   * True to add a 'Delete' button to the top toolbar if the top toolbar is present (defaults to true)
   */
  hasDeleteButton: true,
  
  /**
   * Private.
   * Creates a top toolbar and applies it to 'this'.  Should only be called from inside initComponent
   */
  addTopToolbar: function(paramName) {
    var items = [];
    
    if (this.hasNewButton) {
      this.newButton = new Ext.Toolbar.Button({
        iconCls: 'add',
        text:    'Add',
        scope:   this,
        handler: this.onAdd
      });
      
      items.push(this.newButton);
      items.push('-');
    };
    
    if (this.hasDeleteButton) {
      this.deleteButton = new Ext.Toolbar.Button({
        text:     'Remove selected',
        disabled: true,
        iconCls:  'delete',
        scope:    this,
        handler:  this.onDelete
      });
      
      items.push(this.deleteButton);
    };
    
    Ext.applyIf(this, {
      tbar: items
    });
  },
  
  /**
   * @property windowConfig
   * @type Object
   * Config object passed when creating the New Association window.  Override this to customise
   * the window that appears
   */
  windowConfig: {},
  
  /**
   * Called when the Add button is clicked on the top toolbar
   */
  onAdd: function(btn) {
    if (!this.addWindow) {
      this.addWindow = new Ext.Window(
        Ext.applyIf(this.windowConfig, {
          title:  'New',
          layout: 'fit',
          modal:  true,
          height: 300,
          width:  400,
          items:  [this.form],
          closeAction: 'hide',
          buttons: [
            {
              text:    'Save',
              iconCls: 'save',
              scope:   this,
              handler: this.onSaveNew
            },
            {
              text:    'Cancel',
              iconCls: 'cancel',
              scope:   this,
              handler: this.onCancelNew
            }
          ]
        })
      );
    }
    
    this.addWindow.show();
  },
  
  /**
   * Called when a row is selected and the delete button is clicked
   */
  onDelete: function(btn) {
    var record = this.getSelectionModel().selection.record;
    
    if (record) {
      record.destroy({
        scope:   this,
        success: function() {
          this.store.reload();
        },
        failure: function() {
          Ext.Msg.alert('Delete failed', "Something went wrong while trying to delete - please try again");
          this.store.reload();
        }
      });
    };
    
    this.deleteButton.disable();
  },
  
  /**
   * Called when the user clicks the save button to create a new record
   */
  onSaveNew: function() {
    this.association.create(this.form.getForm().getValues(), {
      scope:   this,
      success: function(modelObj, response) {
        this.store.reload();
        this.addWindow.hide();
      },
      failure: function(modelObj, response) {
        this.form.getForm().clearInvalid();
        this.form.getForm().markInvalid(modelObj.errors.forForm());
      }
    });
  },
  
  /**
   * Called when the user cancels adding a new association model
   */
  onCancelNew: function(paramName) {
    this.addWindow.hide();
  }
});

Ext.reg('hasmany_editorgrid', ExtMVC.view.HasManyEditorGridPanel);


/*!
 * jQuery JavaScript Library v1.3
 * http://jquery.com/
 *
 * Copyright (c) 2009 John Resig
 * Dual licensed under the MIT and GPL licenses.
 * http://docs.jquery.com/License
 *
 * Date: 2009-01-13 12:50:31 -0500 (Tue, 13 Jan 2009)
 * Revision: 6104
 */
(function(){

var 
	// Will speed up references to window, and allows munging its name.
	window = this,
	// Will speed up references to undefined, and allows munging its name.
	undefined,
	// Map over jQuery in case of overwrite
	_jQuery = window.jQuery,
	// Map over the $ in case of overwrite
	_$ = window.$,

	jQuery = window.jQuery = window.$ = function( selector, context ) {
		// The jQuery object is actually just the init constructor 'enhanced'
		return new jQuery.fn.init( selector, context );
	},

	// A simple way to check for HTML strings or ID strings
	// (both of which we optimize for)
	quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
	// Is it a simple selector
	isSimple = /^.[^:#\[\.,]*$/;

jQuery.fn = jQuery.prototype = {
	init: function( selector, context ) {
		// Make sure that a selection was provided
		selector = selector || document;

		// Handle $(DOMElement)
		if ( selector.nodeType ) {
			this[0] = selector;
			this.length = 1;
			this.context = selector;
			return this;
		}
		// Handle HTML strings
		if ( typeof selector === "string" ) {
			// Are we dealing with HTML string or an ID?
			var match = quickExpr.exec( selector );

			// Verify a match, and that no context was specified for #id
			if ( match && (match[1] || !context) ) {

				// HANDLE: $(html) -> $(array)
				if ( match[1] )
					selector = jQuery.clean( [ match[1] ], context );

				// HANDLE: $("#id")
				else {
					var elem = document.getElementById( match[3] );

					// Make sure an element was located
					if ( elem ){
						// Handle the case where IE and Opera return items
						// by name instead of ID
						if ( elem.id != match[3] )
							return jQuery().find( selector );

						// Otherwise, we inject the element directly into the jQuery object
						var ret = jQuery( elem );
						ret.context = document;
						ret.selector = selector;
						return ret;
					}
					selector = [];
				}

			// HANDLE: $(expr, [context])
			// (which is just equivalent to: $(content).find(expr)
			} else
				return jQuery( context ).find( selector );

		// HANDLE: $(function)
		// Shortcut for document ready
		} else if ( jQuery.isFunction( selector ) )
			return jQuery( document ).ready( selector );

		// Make sure that old selector state is passed along
		if ( selector.selector && selector.context ) {
			this.selector = selector.selector;
			this.context = selector.context;
		}

		return this.setArray(jQuery.makeArray(selector));
	},

	// Start with an empty selector
	selector: "",

	// The current version of jQuery being used
	jquery: "1.3",

	// The number of elements contained in the matched element set
	size: function() {
		return this.length;
	},

	// Get the Nth element in the matched element set OR
	// Get the whole matched element set as a clean array
	get: function( num ) {
		return num === undefined ?

			// Return a 'clean' array
			jQuery.makeArray( this ) :

			// Return just the object
			this[ num ];
	},

	// Take an array of elements and push it onto the stack
	// (returning the new matched element set)
	pushStack: function( elems, name, selector ) {
		// Build a new jQuery matched element set
		var ret = jQuery( elems );

		// Add the old object onto the stack (as a reference)
		ret.prevObject = this;

		ret.context = this.context;

		if ( name === "find" )
			ret.selector = this.selector + (this.selector ? " " : "") + selector;
		else if ( name )
			ret.selector = this.selector + "." + name + "(" + selector + ")";

		// Return the newly-formed element set
		return ret;
	},

	// Force the current matched set of elements to become
	// the specified array of elements (destroying the stack in the process)
	// You should use pushStack() in order to do this, but maintain the stack
	setArray: function( elems ) {
		// Resetting the length to 0, then using the native Array push
		// is a super-fast way to populate an object with array-like properties
		this.length = 0;
		Array.prototype.push.apply( this, elems );

		return this;
	},

	// Execute a callback for every element in the matched set.
	// (You can seed the arguments with an array of args, but this is
	// only used internally.)
	each: function( callback, args ) {
		return jQuery.each( this, callback, args );
	},

	// Determine the position of an element within
	// the matched set of elements
	index: function( elem ) {
		// Locate the position of the desired element
		return jQuery.inArray(
			// If it receives a jQuery object, the first element is used
			elem && elem.jquery ? elem[0] : elem
		, this );
	},

	attr: function( name, value, type ) {
		var options = name;

		// Look for the case where we're accessing a style value
		if ( typeof name === "string" )
			if ( value === undefined )
				return this[0] && jQuery[ type || "attr" ]( this[0], name );

			else {
				options = {};
				options[ name ] = value;
			}

		// Check to see if we're setting style values
		return this.each(function(i){
			// Set all the styles
			for ( name in options )
				jQuery.attr(
					type ?
						this.style :
						this,
					name, jQuery.prop( this, options[ name ], type, i, name )
				);
		});
	},

	css: function( key, value ) {
		// ignore negative width and height values
		if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
			value = undefined;
		return this.attr( key, value, "curCSS" );
	},

	text: function( text ) {
		if ( typeof text !== "object" && text != null )
			return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );

		var ret = "";

		jQuery.each( text || this, function(){
			jQuery.each( this.childNodes, function(){
				if ( this.nodeType != 8 )
					ret += this.nodeType != 1 ?
						this.nodeValue :
						jQuery.fn.text( [ this ] );
			});
		});

		return ret;
	},

	wrapAll: function( html ) {
		if ( this[0] ) {
			// The elements to wrap the target around
			var wrap = jQuery( html, this[0].ownerDocument ).clone();

			if ( this[0].parentNode )
				wrap.insertBefore( this[0] );

			wrap.map(function(){
				var elem = this;

				while ( elem.firstChild )
					elem = elem.firstChild;

				return elem;
			}).append(this);
		}

		return this;
	},

	wrapInner: function( html ) {
		return this.each(function(){
			jQuery( this ).contents().wrapAll( html );
		});
	},

	wrap: function( html ) {
		return this.each(function(){
			jQuery( this ).wrapAll( html );
		});
	},

	append: function() {
		return this.domManip(arguments, true, function(elem){
			if (this.nodeType == 1)
				this.appendChild( elem );
		});
	},

	prepend: function() {
		return this.domManip(arguments, true, function(elem){
			if (this.nodeType == 1)
				this.insertBefore( elem, this.firstChild );
		});
	},

	before: function() {
		return this.domManip(arguments, false, function(elem){
			this.parentNode.insertBefore( elem, this );
		});
	},

	after: function() {
		return this.domManip(arguments, false, function(elem){
			this.parentNode.insertBefore( elem, this.nextSibling );
		});
	},

	end: function() {
		return this.prevObject || jQuery( [] );
	},

	// For internal use only.
	// Behaves like an Array's .push method, not like a jQuery method.
	push: [].push,

	find: function( selector ) {
		if ( this.length === 1 && !/,/.test(selector) ) {
			var ret = this.pushStack( [], "find", selector );
			ret.length = 0;
			jQuery.find( selector, this[0], ret );
			return ret;
		} else {
			var elems = jQuery.map(this, function(elem){
				return jQuery.find( selector, elem );
			});

			return this.pushStack( /[^+>] [^+>]/.test( selector ) ?
				jQuery.unique( elems ) :
				elems, "find", selector );
		}
	},

	clone: function( events ) {
		// Do the clone
		var ret = this.map(function(){
			if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
				// IE copies events bound via attachEvent when
				// using cloneNode. Calling detachEvent on the
				// clone will also remove the events from the orignal
				// In order to get around this, we use innerHTML.
				// Unfortunately, this means some modifications to
				// attributes in IE that are actually only stored
				// as properties will not be copied (such as the
				// the name attribute on an input).
				var clone = this.cloneNode(true),
					container = document.createElement("div");
				container.appendChild(clone);
				return jQuery.clean([container.innerHTML])[0];
			} else
				return this.cloneNode(true);
		});

		// Need to set the expando to null on the cloned set if it exists
		// removeData doesn't work here, IE removes it from the original as well
		// this is primarily for IE but the data expando shouldn't be copied over in any browser
		var clone = ret.find("*").andSelf().each(function(){
			if ( this[ expando ] !== undefined )
				this[ expando ] = null;
		});

		// Copy the events from the original to the clone
		if ( events === true )
			this.find("*").andSelf().each(function(i){
				if (this.nodeType == 3)
					return;
				var events = jQuery.data( this, "events" );

				for ( var type in events )
					for ( var handler in events[ type ] )
						jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data );
			});

		// Return the cloned set
		return ret;
	},

	filter: function( selector ) {
		return this.pushStack(
			jQuery.isFunction( selector ) &&
			jQuery.grep(this, function(elem, i){
				return selector.call( elem, i );
			}) ||

			jQuery.multiFilter( selector, jQuery.grep(this, function(elem){
				return elem.nodeType === 1;
			}) ), "filter", selector );
	},

	closest: function( selector ) {
		var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null;

		return this.map(function(){
			var cur = this;
			while ( cur && cur.ownerDocument ) {
				if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) )
					return cur;
				cur = cur.parentNode;
			}
		});
	},

	not: function( selector ) {
		if ( typeof selector === "string" )
			// test special case where just one selector is passed in
			if ( isSimple.test( selector ) )
				return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector );
			else
				selector = jQuery.multiFilter( selector, this );

		var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
		return this.filter(function() {
			return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
		});
	},

	add: function( selector ) {
		return this.pushStack( jQuery.unique( jQuery.merge(
			this.get(),
			typeof selector === "string" ?
				jQuery( selector ) :
				jQuery.makeArray( selector )
		)));
	},

	is: function( selector ) {
		return !!selector && jQuery.multiFilter( selector, this ).length > 0;
	},

	hasClass: function( selector ) {
		return !!selector && this.is( "." + selector );
	},

	val: function( value ) {
		if ( value === undefined ) {			
			var elem = this[0];

			if ( elem ) {
				if( jQuery.nodeName( elem, 'option' ) )
					return (elem.attributes.value || {}).specified ? elem.value : elem.text;
				
				// We need to handle select boxes special
				if ( jQuery.nodeName( elem, "select" ) ) {
					var index = elem.selectedIndex,
						values = [],
						options = elem.options,
						one = elem.type == "select-one";

					// Nothing was selected
					if ( index < 0 )
						return null;

					// Loop through all the selected options
					for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
						var option = options[ i ];

						if ( option.selected ) {
							// Get the specifc value for the option
							value = jQuery(option).val();

							// We don't need an array for one selects
							if ( one )
								return value;

							// Multi-Selects return an array
							values.push( value );
						}
					}

					return values;				
				}

				// Everything else, we just grab the value
				return (elem.value || "").replace(/\r/g, "");

			}

			return undefined;
		}

		if ( typeof value === "number" )
			value += '';

		return this.each(function(){
			if ( this.nodeType != 1 )
				return;

			if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) )
				this.checked = (jQuery.inArray(this.value, value) >= 0 ||
					jQuery.inArray(this.name, value) >= 0);

			else if ( jQuery.nodeName( this, "select" ) ) {
				var values = jQuery.makeArray(value);

				jQuery( "option", this ).each(function(){
					this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
						jQuery.inArray( this.text, values ) >= 0);
				});

				if ( !values.length )
					this.selectedIndex = -1;

			} else
				this.value = value;
		});
	},

	html: function( value ) {
		return value === undefined ?
			(this[0] ?
				this[0].innerHTML :
				null) :
			this.empty().append( value );
	},

	replaceWith: function( value ) {
		return this.after( value ).remove();
	},

	eq: function( i ) {
		return this.slice( i, +i + 1 );
	},

	slice: function() {
		return this.pushStack( Array.prototype.slice.apply( this, arguments ),
			"slice", Array.prototype.slice.call(arguments).join(",") );
	},

	map: function( callback ) {
		return this.pushStack( jQuery.map(this, function(elem, i){
			return callback.call( elem, i, elem );
		}));
	},

	andSelf: function() {
		return this.add( this.prevObject );
	},

	domManip: function( args, table, callback ) {
		if ( this[0] ) {
			var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(),
				scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ),
				first = fragment.firstChild,
				extra = this.length > 1 ? fragment.cloneNode(true) : fragment;

			if ( first )
				for ( var i = 0, l = this.length; i < l; i++ )
					callback.call( root(this[i], first), i > 0 ? extra.cloneNode(true) : fragment );
			
			if ( scripts )
				jQuery.each( scripts, evalScript );
		}

		return this;
		
		function root( elem, cur ) {
			return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ?
				(elem.getElementsByTagName("tbody")[0] ||
				elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
				elem;
		}
	}
};

// Give the init function the jQuery prototype for later instantiation
jQuery.fn.init.prototype = jQuery.fn;

function evalScript( i, elem ) {
	if ( elem.src )
		jQuery.ajax({
			url: elem.src,
			async: false,
			dataType: "script"
		});

	else
		jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );

	if ( elem.parentNode )
		elem.parentNode.removeChild( elem );
}

function now(){
	return +new Date;
}

jQuery.extend = jQuery.fn.extend = function() {
	// copy reference to target object
	var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;

	// Handle a deep copy situation
	if ( typeof target === "boolean" ) {
		deep = target;
		target = arguments[1] || {};
		// skip the boolean and the target
		i = 2;
	}

	// Handle case when target is a string or something (possible in deep copy)
	if ( typeof target !== "object" && !jQuery.isFunction(target) )
		target = {};

	// extend jQuery itself if only one argument is passed
	if ( length == i ) {
		target = this;
		--i;
	}

	for ( ; i < length; i++ )
		// Only deal with non-null/undefined values
		if ( (options = arguments[ i ]) != null )
			// Extend the base object
			for ( var name in options ) {
				var src = target[ name ], copy = options[ name ];

				// Prevent never-ending loop
				if ( target === copy )
					continue;

				// Recurse if we're merging object values
				if ( deep && copy && typeof copy === "object" && !copy.nodeType )
					target[ name ] = jQuery.extend( deep, 
						// Never move original objects, clone them
						src || ( copy.length != null ? [ ] : { } )
					, copy );

				// Don't bring in undefined values
				else if ( copy !== undefined )
					target[ name ] = copy;

			}

	// Return the modified object
	return target;
};

// exclude the following css properties to add px
var	exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
	// cache defaultView
	defaultView = document.defaultView || {},
	toString = Object.prototype.toString;

jQuery.extend({
	noConflict: function( deep ) {
		window.$ = _$;

		if ( deep )
			window.jQuery = _jQuery;

		return jQuery;
	},

	// See test/unit/core.js for details concerning isFunction.
	// Since version 1.3, DOM methods and functions like alert
	// aren't supported. They return false on IE (#2968).
	isFunction: function( obj ) {
		return toString.call(obj) === "[object Function]";
	},

	isArray: function( obj ) {
		return toString.call(obj) === "[object Array]";
	},

	// check if an element is in a (or is an) XML document
	isXMLDoc: function( elem ) {
		return elem.documentElement && !elem.body ||
			elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
	},

	// Evalulates a script in a global context
	globalEval: function( data ) {
		data = jQuery.trim( data );

		if ( data ) {
			// Inspired by code by Andrea Giammarchi
			// http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
			var head = document.getElementsByTagName("head")[0] || document.documentElement,
				script = document.createElement("script");

			script.type = "text/javascript";
			if ( jQuery.support.scriptEval )
				script.appendChild( document.createTextNode( data ) );
			else
				script.text = data;

			// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
			// This arises when a base node is used (#2709).
			head.insertBefore( script, head.firstChild );
			head.removeChild( script );
		}
	},

	nodeName: function( elem, name ) {
		return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
	},

	// args is for internal usage only
	each: function( object, callback, args ) {
		var name, i = 0, length = object.length;

		if ( args ) {
			if ( length === undefined ) {
				for ( name in object )
					if ( callback.apply( object[ name ], args ) === false )
						break;
			} else
				for ( ; i < length; )
					if ( callback.apply( object[ i++ ], args ) === false )
						break;

		// A special, fast, case for the most common use of each
		} else {
			if ( length === undefined ) {
				for ( name in object )
					if ( callback.call( object[ name ], name, object[ name ] ) === false )
						break;
			} else
				for ( var value = object[0];
					i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
		}

		return object;
	},

	prop: function( elem, value, type, i, name ) {
		// Handle executable functions
		if ( jQuery.isFunction( value ) )
			value = value.call( elem, i );

		// Handle passing in a number to a CSS property
		return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ?
			value + "px" :
			value;
	},

	className: {
		// internal only, use addClass("class")
		add: function( elem, classNames ) {
			jQuery.each((classNames || "").split(/\s+/), function(i, className){
				if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
					elem.className += (elem.className ? " " : "") + className;
			});
		},

		// internal only, use removeClass("class")
		remove: function( elem, classNames ) {
			if (elem.nodeType == 1)
				elem.className = classNames !== undefined ?
					jQuery.grep(elem.className.split(/\s+/), function(className){
						return !jQuery.className.has( classNames, className );
					}).join(" ") :
					"";
		},

		// internal only, use hasClass("class")
		has: function( elem, className ) {
			return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
		}
	},

	// A method for quickly swapping in/out CSS properties to get correct calculations
	swap: function( elem, options, callback ) {
		var old = {};
		// Remember the old values, and insert the new ones
		for ( var name in options ) {
			old[ name ] = elem.style[ name ];
			elem.style[ name ] = options[ name ];
		}

		callback.call( elem );

		// Revert the old values
		for ( var name in options )
			elem.style[ name ] = old[ name ];
	},

	css: function( elem, name, force ) {
		if ( name == "width" || name == "height" ) {
			var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];

			function getWH() {
				val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
				var padding = 0, border = 0;
				jQuery.each( which, function() {
					padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
					border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
				});
				val -= Math.round(padding + border);
			}

			if ( jQuery(elem).is(":visible") )
				getWH();
			else
				jQuery.swap( elem, props, getWH );

			return Math.max(0, val);
		}

		return jQuery.curCSS( elem, name, force );
	},

	curCSS: function( elem, name, force ) {
		var ret, style = elem.style;

		// We need to handle opacity special in IE
		if ( name == "opacity" && !jQuery.support.opacity ) {
			ret = jQuery.attr( style, "opacity" );

			return ret == "" ?
				"1" :
				ret;
		}

		// Make sure we're using the right name for getting the float value
		if ( name.match( /float/i ) )
			name = styleFloat;

		if ( !force && style && style[ name ] )
			ret = style[ name ];

		else if ( defaultView.getComputedStyle ) {

			// Only "float" is needed here
			if ( name.match( /float/i ) )
				name = "float";

			name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();

			var computedStyle = defaultView.getComputedStyle( elem, null );

			if ( computedStyle )
				ret = computedStyle.getPropertyValue( name );

			// We should always get a number back from opacity
			if ( name == "opacity" && ret == "" )
				ret = "1";

		} else if ( elem.currentStyle ) {
			var camelCase = name.replace(/\-(\w)/g, function(all, letter){
				return letter.toUpperCase();
			});

			ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];

			// From the awesome hack by Dean Edwards
			// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291

			// If we're not dealing with a regular pixel number
			// but a number that has a weird ending, we need to convert it to pixels
			if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
				// Remember the original values
				var left = style.left, rsLeft = elem.runtimeStyle.left;

				// Put in the new values to get a computed value out
				elem.runtimeStyle.left = elem.currentStyle.left;
				style.left = ret || 0;
				ret = style.pixelLeft + "px";

				// Revert the changed values
				style.left = left;
				elem.runtimeStyle.left = rsLeft;
			}
		}

		return ret;
	},

	clean: function( elems, context, fragment ) {
		context = context || document;

		// !context.createElement fails in IE with an error but returns typeof 'object'
		if ( typeof context.createElement === "undefined" )
			context = context.ownerDocument || context[0] && context[0].ownerDocument || document;

		// If a single string is passed in and it's a single tag
		// just do a createElement and skip the rest
		if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
			var match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
			if ( match )
				return [ context.createElement( match[1] ) ];
		}

		var ret = [], scripts = [], div = context.createElement("div");

		jQuery.each(elems, function(i, elem){
			if ( typeof elem === "number" )
				elem += '';

			if ( !elem )
				return;

			// Convert html string into DOM nodes
			if ( typeof elem === "string" ) {
				// Fix "XHTML"-style tags in all browsers
				elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
					return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
						all :
						front + "></" + tag + ">";
				});

				// Trim whitespace, otherwise indexOf won't work as expected
				var tags = jQuery.trim( elem ).toLowerCase();

				var wrap =
					// option or optgroup
					!tags.indexOf("<opt") &&
					[ 1, "<select multiple='multiple'>", "</select>" ] ||

					!tags.indexOf("<leg") &&
					[ 1, "<fieldset>", "</fieldset>" ] ||

					tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
					[ 1, "<table>", "</table>" ] ||

					!tags.indexOf("<tr") &&
					[ 2, "<table><tbody>", "</tbody></table>" ] ||

				 	// <thead> matched above
					(!tags.indexOf("<td") || !tags.indexOf("<th")) &&
					[ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||

					!tags.indexOf("<col") &&
					[ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||

					// IE can't serialize <link> and <script> tags normally
					!jQuery.support.htmlSerialize &&
					[ 1, "div<div>", "</div>" ] ||

					[ 0, "", "" ];

				// Go to html and back, then peel off extra wrappers
				div.innerHTML = wrap[1] + elem + wrap[2];

				// Move to the right depth
				while ( wrap[0]-- )
					div = div.lastChild;

				// Remove IE's autoinserted <tbody> from table fragments
				if ( !jQuery.support.tbody ) {

					// String was a <table>, *may* have spurious <tbody>
					var tbody = !tags.indexOf("<table") && tags.indexOf("<tbody") < 0 ?
						div.firstChild && div.firstChild.childNodes :

						// String was a bare <thead> or <tfoot>
						wrap[1] == "<table>" && tags.indexOf("<tbody") < 0 ?
							div.childNodes :
							[];

					for ( var j = tbody.length - 1; j >= 0 ; --j )
						if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
							tbody[ j ].parentNode.removeChild( tbody[ j ] );

					}

				// IE completely kills leading whitespace when innerHTML is used
				if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) )
					div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
				
				elem = jQuery.makeArray( div.childNodes );
			}

			if ( elem.nodeType )
				ret.push( elem );
			else
				ret = jQuery.merge( ret, elem );

		});

		if ( fragment ) {
			for ( var i = 0; ret[i]; i++ ) {
				if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
					scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
				} else {
					if ( ret[i].nodeType === 1 )
						ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
					fragment.appendChild( ret[i] );
				}
			}
			
			return scripts;
		}

		return ret;
	},

	attr: function( elem, name, value ) {
		// don't set attributes on text and comment nodes
		if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
			return undefined;

		var notxml = !jQuery.isXMLDoc( elem ),
			// Whether we are setting (or getting)
			set = value !== undefined;

		// Try to normalize/fix the name
		name = notxml && jQuery.props[ name ] || name;

		// Only do all the following if this is a node (faster for style)
		// IE elem.getAttribute passes even for style
		if ( elem.tagName ) {

			// These attributes require special treatment
			var special = /href|src|style/.test( name );

			// Safari mis-reports the default selected property of a hidden option
			// Accessing the parent's selectedIndex property fixes it
			if ( name == "selected" && elem.parentNode )
				elem.parentNode.selectedIndex;

			// If applicable, access the attribute via the DOM 0 way
			if ( name in elem && notxml && !special ) {
				if ( set ){
					// We can't allow the type property to be changed (since it causes problems in IE)
					if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
						throw "type property can't be changed";

					elem[ name ] = value;
				}

				// browsers index elements by id/name on forms, give priority to attributes.
				if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
					return elem.getAttributeNode( name ).nodeValue;

				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
				if ( name == "tabIndex" ) {
					var attributeNode = elem.getAttributeNode( "tabIndex" );
					return attributeNode && attributeNode.specified
						? attributeNode.value
						: elem.nodeName.match(/^(a|area|button|input|object|select|textarea)$/i)
							? 0
							: undefined;
				}

				return elem[ name ];
			}

			if ( !jQuery.support.style && notxml &&  name == "style" )
				return jQuery.attr( elem.style, "cssText", value );

			if ( set )
				// convert the value to a string (all browsers do this but IE) see #1070
				elem.setAttribute( name, "" + value );

			var attr = !jQuery.support.hrefNormalized && notxml && special
					// Some attributes require a special call on IE
					? elem.getAttribute( name, 2 )
					: elem.getAttribute( name );

			// Non-existent attributes return null, we normalize to undefined
			return attr === null ? undefined : attr;
		}

		// elem is actually elem.style ... set the style

		// IE uses filters for opacity
		if ( !jQuery.support.opacity && name == "opacity" ) {
			if ( set ) {
				// IE has trouble with opacity if it does not have layout
				// Force it by setting the zoom level
				elem.zoom = 1;

				// Set the alpha filter to set the opacity
				elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
					(parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
			}

			return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
				(parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
				"";
		}

		name = name.replace(/-([a-z])/ig, function(all, letter){
			return letter.toUpperCase();
		});

		if ( set )
			elem[ name ] = value;

		return elem[ name ];
	},

	trim: function( text ) {
		return (text || "").replace( /^\s+|\s+$/g, "" );
	},

	makeArray: function( array ) {
		var ret = [];

		if( array != null ){
			var i = array.length;
			// The window, strings (and functions) also have 'length'
			if( i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval )
				ret[0] = array;
			else
				while( i )
					ret[--i] = array[i];
		}

		return ret;
	},

	inArray: function( elem, array ) {
		for ( var i = 0, length = array.length; i < length; i++ )
		// Use === because on IE, window == document
			if ( array[ i ] === elem )
				return i;

		return -1;
	},

	merge: function( first, second ) {
		// We have to loop this way because IE & Opera overwrite the length
		// expando of getElementsByTagName
		var i = 0, elem, pos = first.length;
		// Also, we need to make sure that the correct elements are being returned
		// (IE returns comment nodes in a '*' query)
		if ( !jQuery.support.getAll ) {
			while ( (elem = second[ i++ ]) != null )
				if ( elem.nodeType != 8 )
					first[ pos++ ] = elem;

		} else
			while ( (elem = second[ i++ ]) != null )
				first[ pos++ ] = elem;

		return first;
	},

	unique: function( array ) {
		var ret = [], done = {};

		try {

			for ( var i = 0, length = array.length; i < length; i++ ) {
				var id = jQuery.data( array[ i ] );

				if ( !done[ id ] ) {
					done[ id ] = true;
					ret.push( array[ i ] );
				}
			}

		} catch( e ) {
			ret = array;
		}

		return ret;
	},

	grep: function( elems, callback, inv ) {
		var ret = [];

		// Go through the array, only saving the items
		// that pass the validator function
		for ( var i = 0, length = elems.length; i < length; i++ )
			if ( !inv != !callback( elems[ i ], i ) )
				ret.push( elems[ i ] );

		return ret;
	},

	map: function( elems, callback ) {
		var ret = [];

		// Go through the array, translating each of the items to their
		// new value (or values).
		for ( var i = 0, length = elems.length; i < length; i++ ) {
			var value = callback( elems[ i ], i );

			if ( value != null )
				ret[ ret.length ] = value;
		}

		return ret.concat.apply( [], ret );
	}
});

// Use of jQuery.browser is deprecated.
// It's included for backwards compatibility and plugins,
// although they should work to migrate away.

var userAgent = navigator.userAgent.toLowerCase();

// Figure out what browser is being used
jQuery.browser = {
	version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1],
	safari: /webkit/.test( userAgent ),
	opera: /opera/.test( userAgent ),
	msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
	mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
};

jQuery.each({
	parent: function(elem){return elem.parentNode;},
	parents: function(elem){return jQuery.dir(elem,"parentNode");},
	next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
	prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
	nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
	prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
	siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},
	children: function(elem){return jQuery.sibling(elem.firstChild);},
	contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}
}, function(name, fn){
	jQuery.fn[ name ] = function( selector ) {
		var ret = jQuery.map( this, fn );

		if ( selector && typeof selector == "string" )
			ret = jQuery.multiFilter( selector, ret );

		return this.pushStack( jQuery.unique( ret ), name, selector );
	};
});

jQuery.each({
	appendTo: "append",
	prependTo: "prepend",
	insertBefore: "before",
	insertAfter: "after",
	replaceAll: "replaceWith"
}, function(name, original){
	jQuery.fn[ name ] = function() {
		var args = arguments;

		return this.each(function(){
			for ( var i = 0, length = args.length; i < length; i++ )
				jQuery( args[ i ] )[ original ]( this );
		});
	};
});

jQuery.each({
	removeAttr: function( name ) {
		jQuery.attr( this, name, "" );
		if (this.nodeType == 1)
			this.removeAttribute( name );
	},

	addClass: function( classNames ) {
		jQuery.className.add( this, classNames );
	},

	removeClass: function( classNames ) {
		jQuery.className.remove( this, classNames );
	},

	toggleClass: function( classNames, state ) {
		if( typeof state !== "boolean" )
			state = !jQuery.className.has( this, classNames );
		jQuery.className[ state ? "add" : "remove" ]( this, classNames );
	},

	remove: function( selector ) {
		if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
			// Prevent memory leaks
			jQuery( "*", this ).add([this]).each(function(){
				jQuery.event.remove(this);
				jQuery.removeData(this);
			});
			if (this.parentNode)
				this.parentNode.removeChild( this );
		}
	},

	empty: function() {
		// Remove element nodes and prevent memory leaks
		jQuery( ">*", this ).remove();

		// Remove any remaining nodes
		while ( this.firstChild )
			this.removeChild( this.firstChild );
	}
}, function(name, fn){
	jQuery.fn[ name ] = function(){
		return this.each( fn, arguments );
	};
});

// Helper function used by the dimensions and offset modules
function num(elem, prop) {
	return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
}
var expando = "jQuery" + now(), uuid = 0, windowData = {};

jQuery.extend({
	cache: {},

	data: function( elem, name, data ) {
		elem = elem == window ?
			windowData :
			elem;

		var id = elem[ expando ];

		// Compute a unique ID for the element
		if ( !id )
			id = elem[ expando ] = ++uuid;

		// Only generate the data cache if we're
		// trying to access or manipulate it
		if ( name && !jQuery.cache[ id ] )
			jQuery.cache[ id ] = {};

		// Prevent overriding the named cache with undefined values
		if ( data !== undefined )
			jQuery.cache[ id ][ name ] = data;

		// Return the named cache data, or the ID for the element
		return name ?
			jQuery.cache[ id ][ name ] :
			id;
	},

	removeData: function( elem, name ) {
		elem = elem == window ?
			windowData :
			elem;

		var id = elem[ expando ];

		// If we want to remove a specific section of the element's data
		if ( name ) {
			if ( jQuery.cache[ id ] ) {
				// Remove the section of cache data
				delete jQuery.cache[ id ][ name ];

				// If we've removed all the data, remove the element's cache
				name = "";

				for ( name in jQuery.cache[ id ] )
					break;

				if ( !name )
					jQuery.removeData( elem );
			}

		// Otherwise, we want to remove all of the element's data
		} else {
			// Clean up the element expando
			try {
				delete elem[ expando ];
			} catch(e){
				// IE has trouble directly removing the expando
				// but it's ok with using removeAttribute
				if ( elem.removeAttribute )
					elem.removeAttribute( expando );
			}

			// Completely remove the data cache
			delete jQuery.cache[ id ];
		}
	},
	queue: function( elem, type, data ) {
		if ( elem ){
	
			type = (type || "fx") + "queue";
	
			var q = jQuery.data( elem, type );
	
			if ( !q || jQuery.isArray(data) )
				q = jQuery.data( elem, type, jQuery.makeArray(data) );
			else if( data )
				q.push( data );
	
		}
		return q;
	},

	dequeue: function( elem, type ){
		var queue = jQuery.queue( elem, type ),
			fn = queue.shift();
		
		if( !type || type === "fx" )
			fn = queue[0];
			
		if( fn !== undefined )
			fn.call(elem);
	}
});

jQuery.fn.extend({
	data: function( key, value ){
		var parts = key.split(".");
		parts[1] = parts[1] ? "." + parts[1] : "";

		if ( value === undefined ) {
			var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);

			if ( data === undefined && this.length )
				data = jQuery.data( this[0], key );

			return data === undefined && parts[1] ?
				this.data( parts[0] ) :
				data;
		} else
			return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){
				jQuery.data( this, key, value );
			});
	},

	removeData: function( key ){
		return this.each(function(){
			jQuery.removeData( this, key );
		});
	},
	queue: function(type, data){
		if ( typeof type !== "string" ) {
			data = type;
			type = "fx";
		}

		if ( data === undefined )
			return jQuery.queue( this[0], type );

		return this.each(function(){
			var queue = jQuery.queue( this, type, data );
			
			 if( type == "fx" && queue.length == 1 )
				queue[0].call(this);
		});
	},
	dequeue: function(type){
		return this.each(function(){
			jQuery.dequeue( this, type );
		});
	}
});/*!
 * Sizzle CSS Selector Engine - v0.9.1
 *  Copyright 2009, The Dojo Foundation
 *  Released under the MIT, BSD, and GPL Licenses.
 *  More information: http://sizzlejs.com/
 */
(function(){

var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|[^[\]]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,
	done = 0,
	toString = Object.prototype.toString;

var Sizzle = function(selector, context, results, seed) {
	results = results || [];
	context = context || document;

	if ( context.nodeType !== 1 && context.nodeType !== 9 )
		return [];
	
	if ( !selector || typeof selector !== "string" ) {
		return results;
	}

	var parts = [], m, set, checkSet, check, mode, extra, prune = true;
	
	// Reset the position of the chunker regexp (start from head)
	chunker.lastIndex = 0;
	
	while ( (m = chunker.exec(selector)) !== null ) {
		parts.push( m[1] );
		
		if ( m[2] ) {
			extra = RegExp.rightContext;
			break;
		}
	}

	if ( parts.length > 1 && Expr.match.POS.exec( selector ) ) {
		if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
			var later = "", match;

			// Position selectors must be done after the filter
			while ( (match = Expr.match.POS.exec( selector )) ) {
				later += match[0];
				selector = selector.replace( Expr.match.POS, "" );
			}

			set = Sizzle.filter( later, Sizzle( /\s$/.test(selector) ? selector + "*" : selector, context ) );
		} else {
			set = Expr.relative[ parts[0] ] ?
				[ context ] :
				Sizzle( parts.shift(), context );

			while ( parts.length ) {
				var tmpSet = [];

				selector = parts.shift();
				if ( Expr.relative[ selector ] )
					selector += parts.shift();

				for ( var i = 0, l = set.length; i < l; i++ ) {
					Sizzle( selector, set[i], tmpSet );
				}

				set = tmpSet;
			}
		}
	} else {
		var ret = seed ?
			{ expr: parts.pop(), set: makeArray(seed) } :
			Sizzle.find( parts.pop(), parts.length === 1 && context.parentNode ? context.parentNode : context );
		set = Sizzle.filter( ret.expr, ret.set );

		if ( parts.length > 0 ) {
			checkSet = makeArray(set);
		} else {
			prune = false;
		}

		while ( parts.length ) {
			var cur = parts.pop(), pop = cur;

			if ( !Expr.relative[ cur ] ) {
				cur = "";
			} else {
				pop = parts.pop();
			}

			if ( pop == null ) {
				pop = context;
			}

			Expr.relative[ cur ]( checkSet, pop, isXML(context) );
		}
	}

	if ( !checkSet ) {
		checkSet = set;
	}

	if ( !checkSet ) {
		throw "Syntax error, unrecognized expression: " + (cur || selector);
	}

	if ( toString.call(checkSet) === "[object Array]" ) {
		if ( !prune ) {
			results.push.apply( results, checkSet );
		} else if ( context.nodeType === 1 ) {
			for ( var i = 0; checkSet[i] != null; i++ ) {
				if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
					results.push( set[i] );
				}
			}
		} else {
			for ( var i = 0; checkSet[i] != null; i++ ) {
				if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
					results.push( set[i] );
				}
			}
		}
	} else {
		makeArray( checkSet, results );
	}

	if ( extra ) {
		Sizzle( extra, context, results, seed );
	}

	return results;
};

Sizzle.matches = function(expr, set){
	return Sizzle(expr, null, null, set);
};

Sizzle.find = function(expr, context){
	var set, match;

	if ( !expr ) {
		return [];
	}

	for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
		var type = Expr.order[i], match;
		
		if ( (match = Expr.match[ type ].exec( expr )) ) {
			var left = RegExp.leftContext;

			if ( left.substr( left.length - 1 ) !== "\\" ) {
				match[1] = (match[1] || "").replace(/\\/g, "");
				set = Expr.find[ type ]( match, context );
				if ( set != null ) {
					expr = expr.replace( Expr.match[ type ], "" );
					break;
				}
			}
		}
	}

	if ( !set ) {
		set = context.getElementsByTagName("*");
	}

	return {set: set, expr: expr};
};

Sizzle.filter = function(expr, set, inplace, not){
	var old = expr, result = [], curLoop = set, match, anyFound;

	while ( expr && set.length ) {
		for ( var type in Expr.filter ) {
			if ( (match = Expr.match[ type ].exec( expr )) != null ) {
				var filter = Expr.filter[ type ], goodArray = null, goodPos = 0, found, item;
				anyFound = false;

				if ( curLoop == result ) {
					result = [];
				}

				if ( Expr.preFilter[ type ] ) {
					match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not );

					if ( !match ) {
						anyFound = found = true;
					} else if ( match === true ) {
						continue;
					} else if ( match[0] === true ) {
						goodArray = [];
						var last = null, elem;
						for ( var i = 0; (elem = curLoop[i]) !== undefined; i++ ) {
							if ( elem && last !== elem ) {
								goodArray.push( elem );
								last = elem;
							}
						}
					}
				}

				if ( match ) {
					for ( var i = 0; (item = curLoop[i]) !== undefined; i++ ) {
						if ( item ) {
							if ( goodArray && item != goodArray[goodPos] ) {
								goodPos++;
							}
	
							found = filter( item, match, goodPos, goodArray );
							var pass = not ^ !!found;

							if ( inplace && found != null ) {
								if ( pass ) {
									anyFound = true;
								} else {
									curLoop[i] = false;
								}
							} else if ( pass ) {
								result.push( item );
								anyFound = true;
							}
						}
					}
				}

				if ( found !== undefined ) {
					if ( !inplace ) {
						curLoop = result;
					}

					expr = expr.replace( Expr.match[ type ], "" );

					if ( !anyFound ) {
						return [];
					}

					break;
				}
			}
		}

		expr = expr.replace(/\s*,\s*/, "");

		// Improper expression
		if ( expr == old ) {
			if ( anyFound == null ) {
				throw "Syntax error, unrecognized expression: " + expr;
			} else {
				break;
			}
		}

		old = expr;
	}

	return curLoop;
};

var Expr = Sizzle.selectors = {
	order: [ "ID", "NAME", "TAG" ],
	match: {
		ID: /#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
		CLASS: /\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
		NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,
		ATTR: /\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
		TAG: /^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,
		CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
		POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
		PSEUDO: /:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/
	},
	attrMap: {
		"class": "className",
		"for": "htmlFor"
	},
	attrHandle: {
		href: function(elem){
			return elem.getAttribute("href");
		}
	},
	relative: {
		"+": function(checkSet, part){
			for ( var i = 0, l = checkSet.length; i < l; i++ ) {
				var elem = checkSet[i];
				if ( elem ) {
					var cur = elem.previousSibling;
					while ( cur && cur.nodeType !== 1 ) {
						cur = cur.previousSibling;
					}
					checkSet[i] = typeof part === "string" ?
						cur || false :
						cur === part;
				}
			}

			if ( typeof part === "string" ) {
				Sizzle.filter( part, checkSet, true );
			}
		},
		">": function(checkSet, part, isXML){
			if ( typeof part === "string" && !/\W/.test(part) ) {
				part = isXML ? part : part.toUpperCase();

				for ( var i = 0, l = checkSet.length; i < l; i++ ) {
					var elem = checkSet[i];
					if ( elem ) {
						var parent = elem.parentNode;
						checkSet[i] = parent.nodeName === part ? parent : false;
					}
				}
			} else {
				for ( var i = 0, l = checkSet.length; i < l; i++ ) {
					var elem = checkSet[i];
					if ( elem ) {
						checkSet[i] = typeof part === "string" ?
							elem.parentNode :
							elem.parentNode === part;
					}
				}

				if ( typeof part === "string" ) {
					Sizzle.filter( part, checkSet, true );
				}
			}
		},
		"": function(checkSet, part, isXML){
			var doneName = "done" + (done++), checkFn = dirCheck;

			if ( !part.match(/\W/) ) {
				var nodeCheck = part = isXML ? part : part.toUpperCase();
				checkFn = dirNodeCheck;
			}

			checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
		},
		"~": function(checkSet, part, isXML){
			var doneName = "done" + (done++), checkFn = dirCheck;

			if ( typeof part === "string" && !part.match(/\W/) ) {
				var nodeCheck = part = isXML ? part : part.toUpperCase();
				checkFn = dirNodeCheck;
			}

			checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
		}
	},
	find: {
		ID: function(match, context){
			if ( context.getElementById ) {
				var m = context.getElementById(match[1]);
				return m ? [m] : [];
			}
		},
		NAME: function(match, context){
			return context.getElementsByName ? context.getElementsByName(match[1]) : null;
		},
		TAG: function(match, context){
			return context.getElementsByTagName(match[1]);
		}
	},
	preFilter: {
		CLASS: function(match, curLoop, inplace, result, not){
			match = " " + match[1].replace(/\\/g, "") + " ";

			for ( var i = 0; curLoop[i]; i++ ) {
				if ( not ^ (" " + curLoop[i].className + " ").indexOf(match) >= 0 ) {
					if ( !inplace )
						result.push( curLoop[i] );
				} else if ( inplace ) {
					curLoop[i] = false;
				}
			}

			return false;
		},
		ID: function(match){
			return match[1].replace(/\\/g, "");
		},
		TAG: function(match, curLoop){
			for ( var i = 0; !curLoop[i]; i++ ){}
			return isXML(curLoop[i]) ? match[1] : match[1].toUpperCase();
		},
		CHILD: function(match){
			if ( match[1] == "nth" ) {
				// parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
				var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
					match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" ||
					!/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);

				// calculate the numbers (first)n+(last) including if they are negative
				match[2] = (test[1] + (test[2] || 1)) - 0;
				match[3] = test[3] - 0;
			}

			// TODO: Move to normal caching system
			match[0] = "done" + (done++);

			return match;
		},
		ATTR: function(match){
			var name = match[1];
			
			if ( Expr.attrMap[name] ) {
				match[1] = Expr.attrMap[name];
			}

			if ( match[2] === "~=" ) {
				match[4] = " " + match[4] + " ";
			}

			return match;
		},
		PSEUDO: function(match, curLoop, inplace, result, not){
			if ( match[1] === "not" ) {
				// If we're dealing with a complex expression, or a simple one
				if ( match[3].match(chunker).length > 1 ) {
					match[3] = Sizzle(match[3], null, null, curLoop);
				} else {
					var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
					if ( !inplace ) {
						result.push.apply( result, ret );
					}
					return false;
				}
			} else if ( Expr.match.POS.test( match[0] ) ) {
				return true;
			}
			
			return match;
		},
		POS: function(match){
			match.unshift( true );
			return match;
		}
	},
	filters: {
		enabled: function(elem){
			return elem.disabled === false && elem.type !== "hidden";
		},
		disabled: function(elem){
			return elem.disabled === true;
		},
		checked: function(elem){
			return elem.checked === true;
		},
		selected: function(elem){
			// Accessing this property makes selected-by-default
			// options in Safari work properly
			elem.parentNode.selectedIndex;
			return elem.selected === true;
		},
		parent: function(elem){
			return !!elem.firstChild;
		},
		empty: function(elem){
			return !elem.firstChild;
		},
		has: function(elem, i, match){
			return !!Sizzle( match[3], elem ).length;
		},
		header: function(elem){
			return /h\d/i.test( elem.nodeName );
		},
		text: function(elem){
			return "text" === elem.type;
		},
		radio: function(elem){
			return "radio" === elem.type;
		},
		checkbox: function(elem){
			return "checkbox" === elem.type;
		},
		file: function(elem){
			return "file" === elem.type;
		},
		password: function(elem){
			return "password" === elem.type;
		},
		submit: function(elem){
			return "submit" === elem.type;
		},
		image: function(elem){
			return "image" === elem.type;
		},
		reset: function(elem){
			return "reset" === elem.type;
		},
		button: function(elem){
			return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON";
		},
		input: function(elem){
			return /input|select|textarea|button/i.test(elem.nodeName);
		}
	},
	setFilters: {
		first: function(elem, i){
			return i === 0;
		},
		last: function(elem, i, match, array){
			return i === array.length - 1;
		},
		even: function(elem, i){
			return i % 2 === 0;
		},
		odd: function(elem, i){
			return i % 2 === 1;
		},
		lt: function(elem, i, match){
			return i < match[3] - 0;
		},
		gt: function(elem, i, match){
			return i > match[3] - 0;
		},
		nth: function(elem, i, match){
			return match[3] - 0 == i;
		},
		eq: function(elem, i, match){
			return match[3] - 0 == i;
		}
	},
	filter: {
		CHILD: function(elem, match){
			var type = match[1], parent = elem.parentNode;

			var doneName = "child" + parent.childNodes.length;
			
			if ( parent && (!parent[ doneName ] || !elem.nodeIndex) ) {
				var count = 1;

				for ( var node = parent.firstChild; node; node = node.nextSibling ) {
					if ( node.nodeType == 1 ) {
						node.nodeIndex = count++;
					}
				}

				parent[ doneName ] = count - 1;
			}

			if ( type == "first" ) {
				return elem.nodeIndex == 1;
			} else if ( type == "last" ) {
				return elem.nodeIndex == parent[ doneName ];
			} else if ( type == "only" ) {
				return parent[ doneName ] == 1;
			} else if ( type == "nth" ) {
				var add = false, first = match[2], last = match[3];

				if ( first == 1 && last == 0 ) {
					return true;
				}

				if ( first == 0 ) {
					if ( elem.nodeIndex == last ) {
						add = true;
					}
				} else if ( (elem.nodeIndex - last) % first == 0 && (elem.nodeIndex - last) / first >= 0 ) {
					add = true;
				}

				return add;
			}
		},
		PSEUDO: function(elem, match, i, array){
			var name = match[1], filter = Expr.filters[ name ];

			if ( filter ) {
				return filter( elem, i, match, array );
			} else if ( name === "contains" ) {
				return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0;
			} else if ( name === "not" ) {
				var not = match[3];

				for ( var i = 0, l = not.length; i < l; i++ ) {
					if ( not[i] === elem ) {
						return false;
					}
				}

				return true;
			}
		},
		ID: function(elem, match){
			return elem.nodeType === 1 && elem.getAttribute("id") === match;
		},
		TAG: function(elem, match){
			return (match === "*" && elem.nodeType === 1) || elem.nodeName === match;
		},
		CLASS: function(elem, match){
			return match.test( elem.className );
		},
		ATTR: function(elem, match){
			var result = Expr.attrHandle[ match[1] ] ? Expr.attrHandle[ match[1] ]( elem ) : elem[ match[1] ] || elem.getAttribute( match[1] ), value = result + "", type = match[2], check = match[4];
			return result == null ?
				false :
				type === "=" ?
				value === check :
				type === "*=" ?
				value.indexOf(check) >= 0 :
				type === "~=" ?
				(" " + value + " ").indexOf(check) >= 0 :
				!match[4] ?
				result :
				type === "!=" ?
				value != check :
				type === "^=" ?
				value.indexOf(check) === 0 :
				type === "$=" ?
				value.substr(value.length - check.length) === check :
				type === "|=" ?
				value === check || value.substr(0, check.length + 1) === check + "-" :
				false;
		},
		POS: function(elem, match, i, array){
			var name = match[2], filter = Expr.setFilters[ name ];

			if ( filter ) {
				return filter( elem, i, match, array );
			}
		}
	}
};

for ( var type in Expr.match ) {
	Expr.match[ type ] = RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
}

var makeArray = function(array, results) {
	array = Array.prototype.slice.call( array );

	if ( results ) {
		results.push.apply( results, array );
		return results;
	}
	
	return array;
};

// Perform a simple check to determine if the browser is capable of
// converting a NodeList to an array using builtin methods.
try {
	Array.prototype.slice.call( document.documentElement.childNodes );

// Provide a fallback method if it does not work
} catch(e){
	makeArray = function(array, results) {
		var ret = results || [];

		if ( toString.call(array) === "[object Array]" ) {
			Array.prototype.push.apply( ret, array );
		} else {
			if ( typeof array.length === "number" ) {
				for ( var i = 0, l = array.length; i < l; i++ ) {
					ret.push( array[i] );
				}
			} else {
				for ( var i = 0; array[i]; i++ ) {
					ret.push( array[i] );
				}
			}
		}

		return ret;
	};
}

// Check to see if the browser returns elements by name when
// querying by getElementById (and provide a workaround)
(function(){
	// We're going to inject a fake input element with a specified name
	var form = document.createElement("form"),
		id = "script" + (new Date).getTime();
	form.innerHTML = "<input name='" + id + "'/>";

	// Inject it into the root element, check its status, and remove it quickly
	var root = document.documentElement;
	root.insertBefore( form, root.firstChild );

	// The workaround has to do additional checks after a getElementById
	// Which slows things down for other browsers (hence the branching)
	if ( !!document.getElementById( id ) ) {
		Expr.find.ID = function(match, context){
			if ( context.getElementById ) {
				var m = context.getElementById(match[1]);
				return m ? m.id === match[1] || m.getAttributeNode && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
			}
		};

		Expr.filter.ID = function(elem, match){
			var node = elem.getAttributeNode && elem.getAttributeNode("id");
			return elem.nodeType === 1 && node && node.nodeValue === match;
		};
	}

	root.removeChild( form );
})();

(function(){
	// Check to see if the browser returns only elements
	// when doing getElementsByTagName("*")

	// Create a fake element
	var div = document.createElement("div");
	div.appendChild( document.createComment("") );

	// Make sure no comments are found
	if ( div.getElementsByTagName("*").length > 0 ) {
		Expr.find.TAG = function(match, context){
			var results = context.getElementsByTagName(match[1]);

			// Filter out possible comments
			if ( match[1] === "*" ) {
				var tmp = [];

				for ( var i = 0; results[i]; i++ ) {
					if ( results[i].nodeType === 1 ) {
						tmp.push( results[i] );
					}
				}

				results = tmp;
			}

			return results;
		};
	}

	// Check to see if an attribute returns normalized href attributes
	div.innerHTML = "<a href='#'></a>";
	if ( div.firstChild.getAttribute("href") !== "#" ) {
		Expr.attrHandle.href = function(elem){
			return elem.getAttribute("href", 2);
		};
	}
})();

if ( document.querySelectorAll ) (function(){
	var oldSizzle = Sizzle;
	
	Sizzle = function(query, context, extra, seed){
		context = context || document;

		if ( !seed && context.nodeType === 9 ) {
			try {
				return makeArray( context.querySelectorAll(query), extra );
			} catch(e){}
		}
		
		return oldSizzle(query, context, extra, seed);
	};

	Sizzle.find = oldSizzle.find;
	Sizzle.filter = oldSizzle.filter;
	Sizzle.selectors = oldSizzle.selectors;
	Sizzle.matches = oldSizzle.matches;
})();

if ( document.documentElement.getElementsByClassName ) {
	Expr.order.splice(1, 0, "CLASS");
	Expr.find.CLASS = function(match, context) {
		return context.getElementsByClassName(match[1]);
	};
}

function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
		var elem = checkSet[i];
		if ( elem ) {
			elem = elem[dir];
			var match = false;

			while ( elem && elem.nodeType ) {
				var done = elem[doneName];
				if ( done ) {
					match = checkSet[ done ];
					break;
				}

				if ( elem.nodeType === 1 && !isXML )
					elem[doneName] = i;

				if ( elem.nodeName === cur ) {
					match = elem;
					break;
				}

				elem = elem[dir];
			}

			checkSet[i] = match;
		}
	}
}

function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
		var elem = checkSet[i];
		if ( elem ) {
			elem = elem[dir];
			var match = false;

			while ( elem && elem.nodeType ) {
				if ( elem[doneName] ) {
					match = checkSet[ elem[doneName] ];
					break;
				}

				if ( elem.nodeType === 1 ) {
					if ( !isXML )
						elem[doneName] = i;

					if ( typeof cur !== "string" ) {
						if ( elem === cur ) {
							match = true;
							break;
						}

					} else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
						match = elem;
						break;
					}
				}

				elem = elem[dir];
			}

			checkSet[i] = match;
		}
	}
}

var contains = document.compareDocumentPosition ?  function(a, b){
	return a.compareDocumentPosition(b) & 16;
} : function(a, b){
	return a !== b && (a.contains ? a.contains(b) : true);
};

var isXML = function(elem){
	return elem.documentElement && !elem.body ||
		elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
};

// EXPOSE
jQuery.find = Sizzle;
jQuery.filter = Sizzle.filter;
jQuery.expr = Sizzle.selectors;
jQuery.expr[":"] = jQuery.expr.filters;

Sizzle.selectors.filters.hidden = function(elem){
	return "hidden" === elem.type ||
		jQuery.css(elem, "display") === "none" ||
		jQuery.css(elem, "visibility") === "hidden";
};

Sizzle.selectors.filters.visible = function(elem){
	return "hidden" !== elem.type &&
		jQuery.css(elem, "display") !== "none" &&
		jQuery.css(elem, "visibility") !== "hidden";
};

Sizzle.selectors.filters.animated = function(elem){
	return jQuery.grep(jQuery.timers, function(fn){
		return elem === fn.elem;
	}).length;
};

jQuery.multiFilter = function( expr, elems, not ) {
	if ( not ) {
		expr = ":not(" + expr + ")";
	}

	return Sizzle.matches(expr, elems);
};

jQuery.dir = function( elem, dir ){
	var matched = [], cur = elem[dir];
	while ( cur && cur != document ) {
		if ( cur.nodeType == 1 )
			matched.push( cur );
		cur = cur[dir];
	}
	return matched;
};

jQuery.nth = function(cur, result, dir, elem){
	result = result || 1;
	var num = 0;

	for ( ; cur; cur = cur[dir] )
		if ( cur.nodeType == 1 && ++num == result )
			break;

	return cur;
};

jQuery.sibling = function(n, elem){
	var r = [];

	for ( ; n; n = n.nextSibling ) {
		if ( n.nodeType == 1 && n != elem )
			r.push( n );
	}

	return r;
};

return;

window.Sizzle = Sizzle;

})();
/*
 * A number of helper functions used for managing events.
 * Many of the ideas behind this code originated from
 * Dean Edwards' addEvent library.
 */
jQuery.event = {

	// Bind an event to an element
	// Original by Dean Edwards
	add: function(elem, types, handler, data) {
		if ( elem.nodeType == 3 || elem.nodeType == 8 )
			return;

		// For whatever reason, IE has trouble passing the window object
		// around, causing it to be cloned in the process
		if ( elem.setInterval && elem != window )
			elem = window;

		// Make sure that the function being executed has a unique ID
		if ( !handler.guid )
			handler.guid = this.guid++;

		// if data is passed, bind to handler
		if ( data !== undefined ) {
			// Create temporary function pointer to original handler
			var fn = handler;

			// Create unique handler function, wrapped around original handler
			handler = this.proxy( fn );

			// Store data in unique handler
			handler.data = data;
		}

		// Init the element's event structure
		var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
			handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
				// Handle the second event of a trigger and when
				// an event is called after a page has unloaded
				return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
					jQuery.event.handle.apply(arguments.callee.elem, arguments) :
					undefined;
			});
		// Add elem as a property of the handle function
		// This is to prevent a memory leak with non-native
		// event in IE.
		handle.elem = elem;

		// Handle multiple events separated by a space
		// jQuery(...).bind("mouseover mouseout", fn);
		jQuery.each(types.split(/\s+/), function(index, type) {
			// Namespaced event handlers
			var namespaces = type.split(".");
			type = namespaces.shift();
			handler.type = namespaces.slice().sort().join(".");

			// Get the current list of functions bound to this event
			var handlers = events[type];
			
			if ( jQuery.event.specialAll[type] )
				jQuery.event.specialAll[type].setup.call(elem, data, namespaces);

			// Init the event handler queue
			if (!handlers) {
				handlers = events[type] = {};

				// Check for a special event handler
				// Only use addEventListener/attachEvent if the special
				// events handler returns false
				if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
					// Bind the global event handler to the element
					if (elem.addEventListener)
						elem.addEventListener(type, handle, false);
					else if (elem.attachEvent)
						elem.attachEvent("on" + type, handle);
				}
			}

			// Add the function to the element's handler list
			handlers[handler.guid] = handler;

			// Keep track of which events have been used, for global triggering
			jQuery.event.global[type] = true;
		});

		// Nullify elem to prevent memory leaks in IE
		elem = null;
	},

	guid: 1,
	global: {},

	// Detach an event or set of events from an element
	remove: function(elem, types, handler) {
		// don't do events on text and comment nodes
		if ( elem.nodeType == 3 || elem.nodeType == 8 )
			return;

		var events = jQuery.data(elem, "events"), ret, index;

		if ( events ) {
			// Unbind all events for the element
			if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") )
				for ( var type in events )
					this.remove( elem, type + (types || "") );
			else {
				// types is actually an event object here
				if ( types.type ) {
					handler = types.handler;
					types = types.type;
				}

				// Handle multiple events seperated by a space
				// jQuery(...).unbind("mouseover mouseout", fn);
				jQuery.each(types.split(/\s+/), function(index, type){
					// Namespaced event handlers
					var namespaces = type.split(".");
					type = namespaces.shift();
					var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");

					if ( events[type] ) {
						// remove the given handler for the given type
						if ( handler )
							delete events[type][handler.guid];

						// remove all handlers for the given type
						else
							for ( var handle in events[type] )
								// Handle the removal of namespaced events
								if ( namespace.test(events[type][handle].type) )
									delete events[type][handle];
									
						if ( jQuery.event.specialAll[type] )
							jQuery.event.specialAll[type].teardown.call(elem, namespaces);

						// remove generic event handler if no more handlers exist
						for ( ret in events[type] ) break;
						if ( !ret ) {
							if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem, namespaces) === false ) {
								if (elem.removeEventListener)
									elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
								else if (elem.detachEvent)
									elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
							}
							ret = null;
							delete events[type];
						}
					}
				});
			}

			// Remove the expando if it's no longer used
			for ( ret in events ) break;
			if ( !ret ) {
				var handle = jQuery.data( elem, "handle" );
				if ( handle ) handle.elem = null;
				jQuery.removeData( elem, "events" );
				jQuery.removeData( elem, "handle" );
			}
		}
	},

	// bubbling is internal
	trigger: function( event, data, elem, bubbling ) {
		// Event object or event type
		var type = event.type || event;

		if( !bubbling ){
			event = typeof event === "object" ?
				// jQuery.Event object
				event[expando] ? event :
				// Object literal
				jQuery.extend( jQuery.Event(type), event ) :
				// Just the event type (string)
				jQuery.Event(type);

			if ( type.indexOf("!") >= 0 ) {
				event.type = type = type.slice(0, -1);
				event.exclusive = true;
			}

			// Handle a global trigger
			if ( !elem ) {
				// Don't bubble custom events when global (to avoid too much overhead)
				event.stopPropagation();
				// Only trigger if we've ever bound an event for it
				if ( this.global[type] )
					jQuery.each( jQuery.cache, function(){
						if ( this.events && this.events[type] )
							jQuery.event.trigger( event, data, this.handle.elem );
					});
			}

			// Handle triggering a single element

			// don't do events on text and comment nodes
			if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 )
				return undefined;
			
			// Clean up in case it is reused
			event.result = undefined;
			event.target = elem;
			
			// Clone the incoming data, if any
			data = jQuery.makeArray(data);
			data.unshift( event );
		}

		event.currentTarget = elem;

		// Trigger the event, it is assumed that "handle" is a function
		var handle = jQuery.data(elem, "handle");
		if ( handle )
			handle.apply( elem, data );

		// Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
		if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
			event.result = false;

		// Trigger the native events (except for clicks on links)
		if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
			this.triggered = true;
			try {
				elem[ type ]();
			// prevent IE from throwing an error for some hidden elements
			} catch (e) {}
		}

		this.triggered = false;

		if ( !event.isPropagationStopped() ) {
			var parent = elem.parentNode || elem.ownerDocument;
			if ( parent )
				jQuery.event.trigger(event, data, parent, true);
		}
	},

	handle: function(event) {
		// returned undefined or false
		var all, handlers;

		event = arguments[0] = jQuery.event.fix( event || window.event );

		// Namespaced event handlers
		var namespaces = event.type.split(".");
		event.type = namespaces.shift();

		// Cache this now, all = true means, any handler
		all = !namespaces.length && !event.exclusive;
		
		var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");

		handlers = ( jQuery.data(this, "events") || {} )[event.type];

		for ( var j in handlers ) {
			var handler = handlers[j];

			// Filter the functions by class
			if ( all || namespace.test(handler.type) ) {
				// Pass in a reference to the handler function itself
				// So that we can later remove it
				event.handler = handler;
				event.data = handler.data;

				var ret = handler.apply(this, arguments);

				if( ret !== undefined ){
					event.result = ret;
					if ( ret === false ) {
						event.preventDefault();
						event.stopPropagation();
					}
				}

				if( event.isImmediatePropagationStopped() )
					break;

			}
		}
	},

	props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),

	fix: function(event) {
		if ( event[expando] )
			return event;

		// store a copy of the original event object
		// and "clone" to set read-only properties
		var originalEvent = event;
		event = jQuery.Event( originalEvent );

		for ( var i = this.props.length, prop; i; ){
			prop = this.props[ --i ];
			event[ prop ] = originalEvent[ prop ];
		}

		// Fix target property, if necessary
		if ( !event.target )
			event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either

		// check if target is a textnode (safari)
		if ( event.target.nodeType == 3 )
			event.target = event.target.parentNode;

		// Add relatedTarget, if necessary
		if ( !event.relatedTarget && event.fromElement )
			event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;

		// Calculate pageX/Y if missing and clientX/Y available
		if ( event.pageX == null && event.clientX != null ) {
			var doc = document.documentElement, body = document.body;
			event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
			event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
		}

		// Add which for key events
		if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
			event.which = event.charCode || event.keyCode;

		// Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
		if ( !event.metaKey && event.ctrlKey )
			event.metaKey = event.ctrlKey;

		// Add which for click: 1 == left; 2 == middle; 3 == right
		// Note: button is not normalized, so don't use it
		if ( !event.which && event.button )
			event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));

		return event;
	},

	proxy: function( fn, proxy ){
		proxy = proxy || function(){ return fn.apply(this, arguments); };
		// Set the guid of unique handler to the same of original handler, so it can be removed
		proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
		// So proxy can be declared as an argument
		return proxy;
	},

	special: {
		ready: {
			// Make sure the ready event is setup
			setup: bindReady,
			teardown: function() {}
		}
	},
	
	specialAll: {
		live: {
			setup: function( selector, namespaces ){
				jQuery.event.add( this, namespaces[0], liveHandler );
			},
			teardown:  function( namespaces ){
				if ( namespaces.length ) {
					var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
					
					jQuery.each( (jQuery.data(this, "events").live || {}), function(){
						if ( name.test(this.type) )
							remove++;
					});
					
					if ( remove < 1 )
						jQuery.event.remove( this, namespaces[0], liveHandler );
				}
			}
		}
	}
};

jQuery.Event = function( src ){
	// Allow instantiation without the 'new' keyword
	if( !this.preventDefault )
		return new jQuery.Event(src);
	
	// Event object
	if( src && src.type ){
		this.originalEvent = src;
		this.type = src.type;
		this.timeStamp = src.timeStamp;
	// Event type
	}else
		this.type = src;

	if( !this.timeStamp )
		this.timeStamp = now();
	
	// Mark it as fixed
	this[expando] = true;
};

function returnFalse(){
	return false;
}
function returnTrue(){
	return true;
}

// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
jQuery.Event.prototype = {
	preventDefault: function() {
		this.isDefaultPrevented = returnTrue;

		var e = this.originalEvent;
		if( !e )
			return;
		// if preventDefault exists run it on the original event
		if (e.preventDefault)
			e.preventDefault();
		// otherwise set the returnValue property of the original event to false (IE)
		e.returnValue = false;
	},
	stopPropagation: function() {
		this.isPropagationStopped = returnTrue;

		var e = this.originalEvent;
		if( !e )
			return;
		// if stopPropagation exists run it on the original event
		if (e.stopPropagation)
			e.stopPropagation();
		// otherwise set the cancelBubble property of the original event to true (IE)
		e.cancelBubble = true;
	},
	stopImmediatePropagation:function(){
		this.isImmediatePropagationStopped = returnTrue;
		this.stopPropagation();
	},
	isDefaultPrevented: returnFalse,
	isPropagationStopped: returnFalse,
	isImmediatePropagationStopped: returnFalse
};
// Checks if an event happened on an element within another element
// Used in jQuery.event.special.mouseenter and mouseleave handlers
var withinElement = function(event) {
	// Check if mouse(over|out) are still within the same parent element
	var parent = event.relatedTarget;
	// Traverse up the tree
	while ( parent && parent != this )
		try { parent = parent.parentNode; }
		catch(e) { parent = this; }
	
	if( parent != this ){
		// set the correct event type
		event.type = event.data;
		// handle event if we actually just moused on to a non sub-element
		jQuery.event.handle.apply( this, arguments );
	}
};
	
jQuery.each({ 
	mouseover: 'mouseenter', 
	mouseout: 'mouseleave'
}, function( orig, fix ){
	jQuery.event.special[ fix ] = {
		setup: function(){
			jQuery.event.add( this, orig, withinElement, fix );
		},
		teardown: function(){
			jQuery.event.remove( this, orig, withinElement );
		}
	};			   
});

jQuery.fn.extend({
	bind: function( type, data, fn ) {
		return type == "unload" ? this.one(type, data, fn) : this.each(function(){
			jQuery.event.add( this, type, fn || data, fn && data );
		});
	},

	one: function( type, data, fn ) {
		var one = jQuery.event.proxy( fn || data, function(event) {
			jQuery(this).unbind(event, one);
			return (fn || data).apply( this, arguments );
		});
		return this.each(function(){
			jQuery.event.add( this, type, one, fn && data);
		});
	},

	unbind: function( type, fn ) {
		return this.each(function(){
			jQuery.event.remove( this, type, fn );
		});
	},

	trigger: function( type, data ) {
		return this.each(function(){
			jQuery.event.trigger( type, data, this );
		});
	},

	triggerHandler: function( type, data ) {
		if( this[0] ){
			var event = jQuery.Event(type);
			event.preventDefault();
			event.stopPropagation();
			jQuery.event.trigger( event, data, this[0] );
			return event.result;
		}		
	},

	toggle: function( fn ) {
		// Save reference to arguments for access in closure
		var args = arguments, i = 1;

		// link all the functions, so any of them can unbind this click handler
		while( i < args.length )
			jQuery.event.proxy( fn, args[i++] );

		return this.click( jQuery.event.proxy( fn, function(event) {
			// Figure out which function to execute
			this.lastToggle = ( this.lastToggle || 0 ) % i;

			// Make sure that clicks stop
			event.preventDefault();

			// and execute the function
			return args[ this.lastToggle++ ].apply( this, arguments ) || false;
		}));
	},

	hover: function(fnOver, fnOut) {
		return this.mouseenter(fnOver).mouseleave(fnOut);
	},

	ready: function(fn) {
		// Attach the listeners
		bindReady();

		// If the DOM is already ready
		if ( jQuery.isReady )
			// Execute the function immediately
			fn.call( document, jQuery );

		// Otherwise, remember the function for later
		else
			// Add the function to the wait list
			jQuery.readyList.push( fn );

		return this;
	},
	
	live: function( type, fn ){
		var proxy = jQuery.event.proxy( fn );
		proxy.guid += this.selector + type;

		jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy );

		return this;
	},
	
	die: function( type, fn ){
		jQuery(document).unbind( liveConvert(type, this.selector), fn ? { guid: fn.guid + this.selector + type } : null );
		return this;
	}
});

function liveHandler( event ){
	var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"),
		stop = true,
		elems = [];

	jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){
		if ( check.test(fn.type) ) {
			var elem = jQuery(event.target).closest(fn.data)[0];
			if ( elem )
				elems.push({ elem: elem, fn: fn });
		}
	});

	jQuery.each(elems, function(){
		if ( !event.isImmediatePropagationStopped() &&
			this.fn.call(this.elem, event, this.fn.data) === false )
				stop = false;
	});

	return stop;
}

function liveConvert(type, selector){
	return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join(".");
}

jQuery.extend({
	isReady: false,
	readyList: [],
	// Handle when the DOM is ready
	ready: function() {
		// Make sure that the DOM is not already loaded
		if ( !jQuery.isReady ) {
			// Remember that the DOM is ready
			jQuery.isReady = true;

			// If there are functions bound, to execute
			if ( jQuery.readyList ) {
				// Execute all of them
				jQuery.each( jQuery.readyList, function(){
					this.call( document, jQuery );
				});

				// Reset the list of functions
				jQuery.readyList = null;
			}

			// Trigger any bound ready events
			jQuery(document).triggerHandler("ready");
		}
	}
});

var readyBound = false;

function bindReady(){
	if ( readyBound ) return;
	readyBound = true;

	// Mozilla, Opera and webkit nightlies currently support this event
	if ( document.addEventListener ) {
		// Use the handy event callback
		document.addEventListener( "DOMContentLoaded", function(){
			document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
			jQuery.ready();
		}, false );

	// If IE event model is used
	} else if ( document.attachEvent ) {
		// ensure firing before onload,
		// maybe late but safe also for iframes
		document.attachEvent("onreadystatechange", function(){
			if ( document.readyState === "complete" ) {
				document.detachEvent( "onreadystatechange", arguments.callee );
				jQuery.ready();
			}
		});

		// If IE and not an iframe
		// continually check to see if the document is ready
		if ( document.documentElement.doScroll && !window.frameElement ) (function(){
			if ( jQuery.isReady ) return;

			try {
				// If IE is used, use the trick by Diego Perini
				// http://javascript.nwbox.com/IEContentLoaded/
				document.documentElement.doScroll("left");
			} catch( error ) {
				setTimeout( arguments.callee, 0 );
				return;
			}

			// and execute any waiting functions
			jQuery.ready();
		})();
	}

	// A fallback to window.onload, that will always work
	jQuery.event.add( window, "load", jQuery.ready );
}

jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
	"mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
	"change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){

	// Handle event binding
	jQuery.fn[name] = function(fn){
		return fn ? this.bind(name, fn) : this.trigger(name);
	};
});

// Prevent memory leaks in IE
// And prevent errors on refresh with events like mouseover in other browsers
// Window isn't included so as not to unbind existing unload events
jQuery( window ).bind( 'unload', function(){ 
	for ( var id in jQuery.cache )
		// Skip the window
		if ( id != 1 && jQuery.cache[ id ].handle )
			jQuery.event.remove( jQuery.cache[ id ].handle.elem );
}); 
(function(){

	jQuery.support = {};

	var root = document.documentElement,
		script = document.createElement("script"),
		div = document.createElement("div"),
		id = "script" + (new Date).getTime();

	div.style.display = "none";
	div.innerHTML = '   <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';

	var all = div.getElementsByTagName("*"),
		a = div.getElementsByTagName("a")[0];

	// Can't get basic test support
	if ( !all || !all.length || !a ) {
		return;
	}

	jQuery.support = {
		// IE strips leading whitespace when .innerHTML is used
		leadingWhitespace: div.firstChild.nodeType == 3,
		
		// Make sure that tbody elements aren't automatically inserted
		// IE will insert them into empty tables
		tbody: !div.getElementsByTagName("tbody").length,
		
		// Make sure that you can get all elements in an <object> element
		// IE 7 always returns no results
		objectAll: !!div.getElementsByTagName("object")[0]
			.getElementsByTagName("*").length,
		
		// Make sure that link elements get serialized correctly by innerHTML
		// This requires a wrapper element in IE
		htmlSerialize: !!div.getElementsByTagName("link").length,
		
		// Get the style information from getAttribute
		// (IE uses .cssText insted)
		style: /red/.test( a.getAttribute("style") ),
		
		// Make sure that URLs aren't manipulated
		// (IE normalizes it by default)
		hrefNormalized: a.getAttribute("href") === "/a",
		
		// Make sure that element opacity exists
		// (IE uses filter instead)
		opacity: a.style.opacity === "0.5",
		
		// Verify style float existence
		// (IE uses styleFloat instead of cssFloat)
		cssFloat: !!a.style.cssFloat,

		// Will be defined later
		scriptEval: false,
		noCloneEvent: true,
		boxModel: null
	};
	
	script.type = "text/javascript";
	try {
		script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
	} catch(e){}

	root.insertBefore( script, root.firstChild );
	
	// Make sure that the execution of code works by injecting a script
	// tag with appendChild/createTextNode
	// (IE doesn't support this, fails, and uses .text instead)
	if ( window[ id ] ) {
		jQuery.support.scriptEval = true;
		delete window[ id ];
	}

	root.removeChild( script );

	if ( div.attachEvent && div.fireEvent ) {
		div.attachEvent("onclick", function(){
			// Cloning a node shouldn't copy over any
			// bound event handlers (IE does this)
			jQuery.support.noCloneEvent = false;
			div.detachEvent("onclick", arguments.callee);
		});
		div.cloneNode(true).fireEvent("onclick");
	}

	// Figure out if the W3C box model works as expected
	// document.body must exist before we can do this
	jQuery(function(){
		var div = document.createElement("div");
		div.style.width = "1px";
		div.style.paddingLeft = "1px";

		document.body.appendChild( div );
		jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
		document.body.removeChild( div );
	});
})();

var styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat";

jQuery.props = {
	"for": "htmlFor",
	"class": "className",
	"float": styleFloat,
	cssFloat: styleFloat,
	styleFloat: styleFloat,
	readonly: "readOnly",
	maxlength: "maxLength",
	cellspacing: "cellSpacing",
	rowspan: "rowSpan",
	tabindex: "tabIndex"
};
jQuery.fn.extend({
	// Keep a copy of the old load
	_load: jQuery.fn.load,

	load: function( url, params, callback ) {
		if ( typeof url !== "string" )
			return this._load( url );

		var off = url.indexOf(" ");
		if ( off >= 0 ) {
			var selector = url.slice(off, url.length);
			url = url.slice(0, off);
		}

		// Default to a GET request
		var type = "GET";

		// If the second parameter was provided
		if ( params )
			// If it's a function
			if ( jQuery.isFunction( params ) ) {
				// We assume that it's the callback
				callback = params;
				params = null;

			// Otherwise, build a param string
			} else if( typeof params === "object" ) {
				params = jQuery.param( params );
				type = "POST";
			}

		var self = this;

		// Request the remote document
		jQuery.ajax({
			url: url,
			type: type,
			dataType: "html",
			data: params,
			complete: function(res, status){
				// If successful, inject the HTML into all the matched elements
				if ( status == "success" || status == "notmodified" )
					// See if a selector was specified
					self.html( selector ?
						// Create a dummy div to hold the results
						jQuery("<div/>")
							// inject the contents of the document in, removing the scripts
							// to avoid any 'Permission Denied' errors in IE
							.append(res.responseText.replace(/<script(.|\s)*?\/script>/g, ""))

							// Locate the specified elements
							.find(selector) :

						// If not, just inject the full result
						res.responseText );

				if( callback )
					self.each( callback, [res.responseText, status, res] );
			}
		});
		return this;
	},

	serialize: function() {
		return jQuery.param(this.serializeArray());
	},
	serializeArray: function() {
		return this.map(function(){
			return this.elements ? jQuery.makeArray(this.elements) : this;
		})
		.filter(function(){
			return this.name && !this.disabled &&
				(this.checked || /select|textarea/i.test(this.nodeName) ||
					/text|hidden|password/i.test(this.type));
		})
		.map(function(i, elem){
			var val = jQuery(this).val();
			return val == null ? null :
				jQuery.isArray(val) ?
					jQuery.map( val, function(val, i){
						return {name: elem.name, value: val};
					}) :
					{name: elem.name, value: val};
		}).get();
	}
});

// Attach a bunch of functions for handling common AJAX events
jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
	jQuery.fn[o] = function(f){
		return this.bind(o, f);
	};
});

var jsc = now();

jQuery.extend({
  
	get: function( url, data, callback, type ) {
		// shift arguments if data argument was ommited
		if ( jQuery.isFunction( data ) ) {
			callback = data;
			data = null;
		}

		return jQuery.ajax({
			type: "GET",
			url: url,
			data: data,
			success: callback,
			dataType: type
		});
	},

	getScript: function( url, callback ) {
		return jQuery.get(url, null, callback, "script");
	},

	getJSON: function( url, data, callback ) {
		return jQuery.get(url, data, callback, "json");
	},

	post: function( url, data, callback, type ) {
		if ( jQuery.isFunction( data ) ) {
			callback = data;
			data = {};
		}

		return jQuery.ajax({
			type: "POST",
			url: url,
			data: data,
			success: callback,
			dataType: type
		});
	},

	ajaxSetup: function( settings ) {
		jQuery.extend( jQuery.ajaxSettings, settings );
	},

	ajaxSettings: {
		url: location.href,
		global: true,
		type: "GET",
		contentType: "application/x-www-form-urlencoded",
		processData: true,
		async: true,
		/*
		timeout: 0,
		data: null,
		username: null,
		password: null,
		*/
		// Create the request object; Microsoft failed to properly
		// implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
		// This function can be overriden by calling jQuery.ajaxSetup
		xhr:function(){
			return window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
		},
		accepts: {
			xml: "application/xml, text/xml",
			html: "text/html",
			script: "text/javascript, application/javascript",
			json: "application/json, text/javascript",
			text: "text/plain",
			_default: "*/*"
		}
	},

	// Last-Modified header cache for next request
	lastModified: {},

	ajax: function( s ) {
		// Extend the settings, but re-extend 's' so that it can be
		// checked again later (in the test suite, specifically)
		s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));

		var jsonp, jsre = /=\?(&|$)/g, status, data,
			type = s.type.toUpperCase();

		// convert data if not already a string
		if ( s.data && s.processData && typeof s.data !== "string" )
			s.data = jQuery.param(s.data);

		// Handle JSONP Parameter Callbacks
		if ( s.dataType == "jsonp" ) {
			if ( type == "GET" ) {
				if ( !s.url.match(jsre) )
					s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
			} else if ( !s.data || !s.data.match(jsre) )
				s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
			s.dataType = "json";
		}

		// Build temporary JSONP function
		if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
			jsonp = "jsonp" + jsc++;

			// Replace the =? sequence both in the query string and the data
			if ( s.data )
				s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
			s.url = s.url.replace(jsre, "=" + jsonp + "$1");

			// We need to make sure
			// that a JSONP style response is executed properly
			s.dataType = "script";

			// Handle JSONP-style loading
			window[ jsonp ] = function(tmp){
				data = tmp;
				success();
				complete();
				// Garbage collect
				window[ jsonp ] = undefined;
				try{ delete window[ jsonp ]; } catch(e){}
				if ( head )
					head.removeChild( script );
			};
		}

		if ( s.dataType == "script" && s.cache == null )
			s.cache = false;

		if ( s.cache === false && type == "GET" ) {
			var ts = now();
			// try replacing _= if it is there
			var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
			// if nothing was replaced, add timestamp to the end
			s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
		}

		// If data is available, append data to url for get requests
		if ( s.data && type == "GET" ) {
			s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;

			// IE likes to send both get and post data, prevent this
			s.data = null;
		}

		// Watch for a new set of requests
		if ( s.global && ! jQuery.active++ )
			jQuery.event.trigger( "ajaxStart" );

		// Matches an absolute URL, and saves the domain
		var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec( s.url );

		// If we're requesting a remote document
		// and trying to load JSON or Script with a GET
		if ( s.dataType == "script" && type == "GET" && parts
			&& ( parts[1] && parts[1] != location.protocol || parts[2] != location.host )){

			var head = document.getElementsByTagName("head")[0];
			var script = document.createElement("script");
			script.src = s.url;
			if (s.scriptCharset)
				script.charset = s.scriptCharset;

			// Handle Script loading
			if ( !jsonp ) {
				var done = false;

				// Attach handlers for all browsers
				script.onload = script.onreadystatechange = function(){
					if ( !done && (!this.readyState ||
							this.readyState == "loaded" || this.readyState == "complete") ) {
						done = true;
						success();
						complete();
						head.removeChild( script );
					}
				};
			}

			head.appendChild(script);

			// We handle everything using the script element injection
			return undefined;
		}

		var requestDone = false;

		// Create the request object
		var xhr = s.xhr();

		// Open the socket
		// Passing null username, generates a login popup on Opera (#2865)
		if( s.username )
			xhr.open(type, s.url, s.async, s.username, s.password);
		else
			xhr.open(type, s.url, s.async);

		// Need an extra try/catch for cross domain requests in Firefox 3
		try {
			// Set the correct header, if data is being sent
			if ( s.data )
				xhr.setRequestHeader("Content-Type", s.contentType);

			// Set the If-Modified-Since header, if ifModified mode.
			if ( s.ifModified )
				xhr.setRequestHeader("If-Modified-Since",
					jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );

			// Set header so the called script knows that it's an XMLHttpRequest
			xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

			// Set the Accepts header for the server, depending on the dataType
			xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
				s.accepts[ s.dataType ] + ", */*" :
				s.accepts._default );
		} catch(e){}

		// Allow custom headers/mimetypes and early abort
		if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
			// Handle the global AJAX counter
			if ( s.global && ! --jQuery.active )
				jQuery.event.trigger( "ajaxStop" );
			// close opended socket
			xhr.abort();
			return false;
		}

		if ( s.global )
			jQuery.event.trigger("ajaxSend", [xhr, s]);

		// Wait for a response to come back
		var onreadystatechange = function(isTimeout){
			// The request was aborted, clear the interval and decrement jQuery.active
			if (xhr.readyState == 0) {
				if (ival) {
					// clear poll interval
					clearInterval(ival);
					ival = null;
					// Handle the global AJAX counter
					if ( s.global && ! --jQuery.active )
						jQuery.event.trigger( "ajaxStop" );
				}
			// The transfer is complete and the data is available, or the request timed out
			} else if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
				requestDone = true;

				// clear poll interval
				if (ival) {
					clearInterval(ival);
					ival = null;
				}

				status = isTimeout == "timeout" ? "timeout" :
					!jQuery.httpSuccess( xhr ) ? "error" :
					s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" :
					"success";

				if ( status == "success" ) {
					// Watch for, and catch, XML document parse errors
					try {
						// process the data (runs the xml through httpData regardless of callback)
						data = jQuery.httpData( xhr, s.dataType, s );
					} catch(e) {
						status = "parsererror";
					}
				}

				// Make sure that the request was successful or notmodified
				if ( status == "success" ) {
					// Cache Last-Modified header, if ifModified mode.
					var modRes;
					try {
						modRes = xhr.getResponseHeader("Last-Modified");
					} catch(e) {} // swallow exception thrown by FF if header is not available

					if ( s.ifModified && modRes )
						jQuery.lastModified[s.url] = modRes;

					// JSONP handles its own success callback
					if ( !jsonp )
						success();
				} else
					jQuery.handleError(s, xhr, status);

				// Fire the complete handlers
				complete();

				// Stop memory leaks
				if ( s.async )
					xhr = null;
			}
		};

		if ( s.async ) {
			// don't attach the handler to the request, just poll it instead
			var ival = setInterval(onreadystatechange, 13);

			// Timeout checker
			if ( s.timeout > 0 )
				setTimeout(function(){
					// Check to see if the request is still happening
					if ( xhr ) {
						if( !requestDone )
							onreadystatechange( "timeout" );

						// Cancel the request
						if ( xhr )
							xhr.abort();
					}
				}, s.timeout);
		}

		// Send the data
		try {
			xhr.send(s.data);
		} catch(e) {
			jQuery.handleError(s, xhr, null, e);
		}

		// firefox 1.5 doesn't fire statechange for sync requests
		if ( !s.async )
			onreadystatechange();

		function success(){
			// If a local callback was specified, fire it and pass it the data
			if ( s.success )
				s.success( data, status );

			// Fire the global callback
			if ( s.global )
				jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
		}

		function complete(){
			// Process result
			if ( s.complete )
				s.complete(xhr, status);

			// The request was completed
			if ( s.global )
				jQuery.event.trigger( "ajaxComplete", [xhr, s] );

			// Handle the global AJAX counter
			if ( s.global && ! --jQuery.active )
				jQuery.event.trigger( "ajaxStop" );
		}

		// return XMLHttpRequest to allow aborting the request etc.
		return xhr;
	},

	handleError: function( s, xhr, status, e ) {
		// If a local callback was specified, fire it
		if ( s.error ) s.error( xhr, status, e );

		// Fire the global callback
		if ( s.global )
			jQuery.event.trigger( "ajaxError", [xhr, s, e] );
	},

	// Counter for holding the number of active queries
	active: 0,

	// Determines if an XMLHttpRequest was successful or not
	httpSuccess: function( xhr ) {
		try {
			// IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
			return !xhr.status && location.protocol == "file:" ||
				( xhr.status >= 200 && xhr.status < 300 ) || xhr.status == 304 || xhr.status == 1223;
		} catch(e){}
		return false;
	},

	// Determines if an XMLHttpRequest returns NotModified
	httpNotModified: function( xhr, url ) {
		try {
			var xhrRes = xhr.getResponseHeader("Last-Modified");

			// Firefox always returns 200. check Last-Modified date
			return xhr.status == 304 || xhrRes == jQuery.lastModified[url];
		} catch(e){}
		return false;
	},

	httpData: function( xhr, type, s ) {
		var ct = xhr.getResponseHeader("content-type"),
			xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0,
			data = xml ? xhr.responseXML : xhr.responseText;

		if ( xml && data.documentElement.tagName == "parsererror" )
			throw "parsererror";
			
		// Allow a pre-filtering function to sanitize the response
		// s != null is checked to keep backwards compatibility
		if( s && s.dataFilter )
			data = s.dataFilter( data, type );

		// The filter can actually parse the response
		if( typeof data === "string" ){

			// If the type is "script", eval it in global context
			if ( type == "script" )
				jQuery.globalEval( data );

			// Get the JavaScript object, if JSON is used.
			if ( type == "json" )
				data = window["eval"]("(" + data + ")");
		}
		
		return data;
	},

	// Serialize an array of form elements or a set of
	// key/values into a query string
	param: function( a ) {
		var s = [ ];

		function add( key, value ){
			s[ s.length ] = encodeURIComponent(key) + '=' + encodeURIComponent(value);
		};

		// If an array was passed in, assume that it is an array
		// of form elements
		if ( jQuery.isArray(a) || a.jquery )
			// Serialize the form elements
			jQuery.each( a, function(){
				add( this.name, this.value );
			});

		// Otherwise, assume that it's an object of key/value pairs
		else
			// Serialize the key/values
			for ( var j in a )
				// If the value is an array then the key names need to be repeated
				if ( jQuery.isArray(a[j]) )
					jQuery.each( a[j], function(){
						add( j, this );
					});
				else
					add( j, jQuery.isFunction(a[j]) ? a[j]() : a[j] );

		// Return the resulting serialization
		return s.join("&").replace(/%20/g, "+");
	}

});
var elemdisplay = {},
	fxAttrs = [
		// height animations
		[ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
		// width animations
		[ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
		// opacity animations
		[ "opacity" ]
	];

function genFx( type, num ){
	var obj = {};
	jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function(){
		obj[ this ] = type;
	});
	return obj;
}

jQuery.fn.extend({
	show: function(speed,callback){
		if ( speed ) {
			return this.animate( genFx("show", 3), speed, callback);
		} else {
			for ( var i = 0, l = this.length; i < l; i++ ){
				var old = jQuery.data(this[i], "olddisplay");
				
				this[i].style.display = old || "";
				
				if ( jQuery.css(this[i], "display") === "none" ) {
					var tagName = this[i].tagName, display;
					
					if ( elemdisplay[ tagName ] ) {
						display = elemdisplay[ tagName ];
					} else {
						var elem = jQuery("<" + tagName + " />").appendTo("body");
						
						display = elem.css("display");
						if ( display === "none" )
							display = "block";
						
						elem.remove();
						
						elemdisplay[ tagName ] = display;
					}
					
					this[i].style.display = jQuery.data(this[i], "olddisplay", display);
				}
			}
			
			return this;
		}
	},

	hide: function(speed,callback){
		if ( speed ) {
			return this.animate( genFx("hide", 3), speed, callback);
		} else {
			for ( var i = 0, l = this.length; i < l; i++ ){
				var old = jQuery.data(this[i], "olddisplay");
				if ( !old && old !== "none" )
					jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display"));
				this[i].style.display = "none";
			}
			return this;
		}
	},

	// Save the old toggle function
	_toggle: jQuery.fn.toggle,

	toggle: function( fn, fn2 ){
		var bool = typeof fn === "boolean";

		return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
			this._toggle.apply( this, arguments ) :
			fn == null || bool ?
				this.each(function(){
					var state = bool ? fn : jQuery(this).is(":hidden");
					jQuery(this)[ state ? "show" : "hide" ]();
				}) :
				this.animate(genFx("toggle", 3), fn, fn2);
	},

	fadeTo: function(speed,to,callback){
		return this.animate({opacity: to}, speed, callback);
	},

	animate: function( prop, speed, easing, callback ) {
		var optall = jQuery.speed(speed, easing, callback);

		return this[ optall.queue === false ? "each" : "queue" ](function(){
		
			var opt = jQuery.extend({}, optall), p,
				hidden = this.nodeType == 1 && jQuery(this).is(":hidden"),
				self = this;
	
			for ( p in prop ) {
				if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )
					return opt.complete.call(this);

				if ( ( p == "height" || p == "width" ) && this.style ) {
					// Store display property
					opt.display = jQuery.css(this, "display");

					// Make sure that nothing sneaks out
					opt.overflow = this.style.overflow;
				}
			}

			if ( opt.overflow != null )
				this.style.overflow = "hidden";

			opt.curAnim = jQuery.extend({}, prop);

			jQuery.each( prop, function(name, val){
				var e = new jQuery.fx( self, opt, name );

				if ( /toggle|show|hide/.test(val) )
					e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );
				else {
					var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
						start = e.cur(true) || 0;

					if ( parts ) {
						var end = parseFloat(parts[2]),
							unit = parts[3] || "px";

						// We need to compute starting value
						if ( unit != "px" ) {
							self.style[ name ] = (end || 1) + unit;
							start = ((end || 1) / e.cur(true)) * start;
							self.style[ name ] = start + unit;
						}

						// If a +=/-= token was provided, we're doing a relative animation
						if ( parts[1] )
							end = ((parts[1] == "-=" ? -1 : 1) * end) + start;

						e.custom( start, end, unit );
					} else
						e.custom( start, val, "" );
				}
			});

			// For JS strict compliance
			return true;
		});
	},

	stop: function(clearQueue, gotoEnd){
		var timers = jQuery.timers;

		if (clearQueue)
			this.queue([]);

		this.each(function(){
			// go in reverse order so anything added to the queue during the loop is ignored
			for ( var i = timers.length - 1; i >= 0; i-- )
				if ( timers[i].elem == this ) {
					if (gotoEnd)
						// force the next step to be the last
						timers[i](true);
					timers.splice(i, 1);
				}
		});

		// start the next in the queue if the last step wasn't forced
		if (!gotoEnd)
			this.dequeue();

		return this;
	}

});

// Generate shortcuts for custom animations
jQuery.each({
	slideDown: genFx("show", 1),
	slideUp: genFx("hide", 1),
	slideToggle: genFx("toggle", 1),
	fadeIn: { opacity: "show" },
	fadeOut: { opacity: "hide" }
}, function( name, props ){
	jQuery.fn[ name ] = function( speed, callback ){
		return this.animate( props, speed, callback );
	};
});

jQuery.extend({

	speed: function(speed, easing, fn) {
		var opt = typeof speed === "object" ? speed : {
			complete: fn || !fn && easing ||
				jQuery.isFunction( speed ) && speed,
			duration: speed,
			easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
		};

		opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
			jQuery.fx.speeds[opt.duration] || jQuery.fx.speeds._default;

		// Queueing
		opt.old = opt.complete;
		opt.complete = function(){
			if ( opt.queue !== false )
				jQuery(this).dequeue();
			if ( jQuery.isFunction( opt.old ) )
				opt.old.call( this );
		};

		return opt;
	},

	easing: {
		linear: function( p, n, firstNum, diff ) {
			return firstNum + diff * p;
		},
		swing: function( p, n, firstNum, diff ) {
			return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
		}
	},

	timers: [],
	timerId: null,

	fx: function( elem, options, prop ){
		this.options = options;
		this.elem = elem;
		this.prop = prop;

		if ( !options.orig )
			options.orig = {};
	}

});

jQuery.fx.prototype = {

	// Simple function for setting a style value
	update: function(){
		if ( this.options.step )
			this.options.step.call( this.elem, this.now, this );

		(jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );

		// Set display property to block for height/width animations
		if ( ( this.prop == "height" || this.prop == "width" ) && this.elem.style )
			this.elem.style.display = "block";
	},

	// Get the current size
	cur: function(force){
		if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) )
			return this.elem[ this.prop ];

		var r = parseFloat(jQuery.css(this.elem, this.prop, force));
		return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
	},

	// Start an animation from one number to another
	custom: function(from, to, unit){
		this.startTime = now();
		this.start = from;
		this.end = to;
		this.unit = unit || this.unit || "px";
		this.now = this.start;
		this.pos = this.state = 0;

		var self = this;
		function t(gotoEnd){
			return self.step(gotoEnd);
		}

		t.elem = this.elem;

		jQuery.timers.push(t);

		if ( t() && jQuery.timerId == null ) {
			jQuery.timerId = setInterval(function(){
				var timers = jQuery.timers;

				for ( var i = 0; i < timers.length; i++ )
					if ( !timers[i]() )
						timers.splice(i--, 1);

				if ( !timers.length ) {
					clearInterval( jQuery.timerId );
					jQuery.timerId = null;
				}
			}, 13);
		}
	},

	// Simple 'show' function
	show: function(){
		// Remember where we started, so that we can go back to it later
		this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
		this.options.show = true;

		// Begin the animation
		// Make sure that we start at a small width/height to avoid any
		// flash of content
		this.custom(this.prop == "width" || this.prop == "height" ? 1 : 0, this.cur());

		// Start by showing the element
		jQuery(this.elem).show();
	},

	// Simple 'hide' function
	hide: function(){
		// Remember where we started, so that we can go back to it later
		this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
		this.options.hide = true;

		// Begin the animation
		this.custom(this.cur(), 0);
	},

	// Each step of an animation
	step: function(gotoEnd){
		var t = now();

		if ( gotoEnd || t >= this.options.duration + this.startTime ) {
			this.now = this.end;
			this.pos = this.state = 1;
			this.update();

			this.options.curAnim[ this.prop ] = true;

			var done = true;
			for ( var i in this.options.curAnim )
				if ( this.options.curAnim[i] !== true )
					done = false;

			if ( done ) {
				if ( this.options.display != null ) {
					// Reset the overflow
					this.elem.style.overflow = this.options.overflow;

					// Reset the display
					this.elem.style.display = this.options.display;
					if ( jQuery.css(this.elem, "display") == "none" )
						this.elem.style.display = "block";
				}

				// Hide the element if the "hide" operation was done
				if ( this.options.hide )
					jQuery(this.elem).hide();

				// Reset the properties, if the item has been hidden or shown
				if ( this.options.hide || this.options.show )
					for ( var p in this.options.curAnim )
						jQuery.attr(this.elem.style, p, this.options.orig[p]);
			}

			if ( done )
				// Execute the complete function
				this.options.complete.call( this.elem );

			return false;
		} else {
			var n = t - this.startTime;
			this.state = n / this.options.duration;

			// Perform the easing function, defaults to swing
			this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
			this.now = this.start + ((this.end - this.start) * this.pos);

			// Perform the next step of the animation
			this.update();
		}

		return true;
	}

};

jQuery.extend( jQuery.fx, {
	speeds:{
		slow: 600,
 		fast: 200,
 		// Default speed
 		_default: 400
	},
	step: {

		opacity: function(fx){
			jQuery.attr(fx.elem.style, "opacity", fx.now);
		},

		_default: function(fx){
			if ( fx.elem.style && fx.elem.style[ fx.prop ] != null )
				fx.elem.style[ fx.prop ] = fx.now + fx.unit;
			else
				fx.elem[ fx.prop ] = fx.now;
		}
	}
});
if ( document.documentElement["getBoundingClientRect"] )
	jQuery.fn.offset = function() {
		if ( !this[0] ) return { top: 0, left: 0 };
		if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
		var box  = this[0].getBoundingClientRect(), doc = this[0].ownerDocument, body = doc.body, docElem = doc.documentElement,
			clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
			top  = box.top  + (self.pageYOffset || jQuery.boxModel && docElem.scrollTop  || body.scrollTop ) - clientTop,
			left = box.left + (self.pageXOffset || jQuery.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
		return { top: top, left: left };
	};
else 
	jQuery.fn.offset = function() {
		if ( !this[0] ) return { top: 0, left: 0 };
		if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
		jQuery.offset.initialized || jQuery.offset.initialize();

		var elem = this[0], offsetParent = elem.offsetParent, prevOffsetParent = elem,
			doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement,
			body = doc.body, defaultView = doc.defaultView,
			prevComputedStyle = defaultView.getComputedStyle(elem, null),
			top = elem.offsetTop, left = elem.offsetLeft;

		while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
			computedStyle = defaultView.getComputedStyle(elem, null);
			top -= elem.scrollTop, left -= elem.scrollLeft;
			if ( elem === offsetParent ) {
				top += elem.offsetTop, left += elem.offsetLeft;
				if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName)) )
					top  += parseInt( computedStyle.borderTopWidth,  10) || 0,
					left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
				prevOffsetParent = offsetParent, offsetParent = elem.offsetParent;
			}
			if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" )
				top  += parseInt( computedStyle.borderTopWidth,  10) || 0,
				left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
			prevComputedStyle = computedStyle;
		}

		if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" )
			top  += body.offsetTop,
			left += body.offsetLeft;

		if ( prevComputedStyle.position === "fixed" )
			top  += Math.max(docElem.scrollTop, body.scrollTop),
			left += Math.max(docElem.scrollLeft, body.scrollLeft);

		return { top: top, left: left };
	};

jQuery.offset = {
	initialize: function() {
		if ( this.initialized ) return;
		var body = document.body, container = document.createElement('div'), innerDiv, checkDiv, table, td, rules, prop, bodyMarginTop = body.style.marginTop,
			html = '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"cellpadding="0"cellspacing="0"><tr><td></td></tr></table>';

		rules = { position: 'absolute', top: 0, left: 0, margin: 0, border: 0, width: '1px', height: '1px', visibility: 'hidden' };
		for ( prop in rules ) container.style[prop] = rules[prop];

		container.innerHTML = html;
		body.insertBefore(container, body.firstChild);
		innerDiv = container.firstChild, checkDiv = innerDiv.firstChild, td = innerDiv.nextSibling.firstChild.firstChild;

		this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
		this.doesAddBorderForTableAndCells = (td.offsetTop === 5);

		innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative';
		this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);

		body.style.marginTop = '1px';
		this.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0);
		body.style.marginTop = bodyMarginTop;

		body.removeChild(container);
		this.initialized = true;
	},

	bodyOffset: function(body) {
		jQuery.offset.initialized || jQuery.offset.initialize();
		var top = body.offsetTop, left = body.offsetLeft;
		if ( jQuery.offset.doesNotIncludeMarginInBodyOffset )
			top  += parseInt( jQuery.curCSS(body, 'marginTop',  true), 10 ) || 0,
			left += parseInt( jQuery.curCSS(body, 'marginLeft', true), 10 ) || 0;
		return { top: top, left: left };
	}
};


jQuery.fn.extend({
	position: function() {
		var left = 0, top = 0, results;

		if ( this[0] ) {
			// Get *real* offsetParent
			var offsetParent = this.offsetParent(),

			// Get correct offsets
			offset       = this.offset(),
			parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();

			// Subtract element margins
			// note: when an element has margin: auto the offsetLeft and marginLeft 
			// are the same in Safari causing offset.left to incorrectly be 0
			offset.top  -= num( this, 'marginTop'  );
			offset.left -= num( this, 'marginLeft' );

			// Add offsetParent borders
			parentOffset.top  += num( offsetParent, 'borderTopWidth'  );
			parentOffset.left += num( offsetParent, 'borderLeftWidth' );

			// Subtract the two offsets
			results = {
				top:  offset.top  - parentOffset.top,
				left: offset.left - parentOffset.left
			};
		}

		return results;
	},

	offsetParent: function() {
		var offsetParent = this[0].offsetParent || document.body;
		while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') )
			offsetParent = offsetParent.offsetParent;
		return jQuery(offsetParent);
	}
});


// Create scrollLeft and scrollTop methods
jQuery.each( ['Left', 'Top'], function(i, name) {
	var method = 'scroll' + name;
	
	jQuery.fn[ method ] = function(val) {
		if (!this[0]) return null;

		return val !== undefined ?

			// Set the scroll offset
			this.each(function() {
				this == window || this == document ?
					window.scrollTo(
						!i ? val : jQuery(window).scrollLeft(),
						 i ? val : jQuery(window).scrollTop()
					) :
					this[ method ] = val;
			}) :

			// Return the scroll offset
			this[0] == window || this[0] == document ?
				self[ i ? 'pageYOffset' : 'pageXOffset' ] ||
					jQuery.boxModel && document.documentElement[ method ] ||
					document.body[ method ] :
				this[0][ method ];
	};
});
// Create innerHeight, innerWidth, outerHeight and outerWidth methods
jQuery.each([ "Height", "Width" ], function(i, name){

	var tl = i ? "Left"  : "Top",  // top or left
		br = i ? "Right" : "Bottom"; // bottom or right

	// innerHeight and innerWidth
	jQuery.fn["inner" + name] = function(){
		return this[ name.toLowerCase() ]() +
			num(this, "padding" + tl) +
			num(this, "padding" + br);
	};

	// outerHeight and outerWidth
	jQuery.fn["outer" + name] = function(margin) {
		return this["inner" + name]() +
			num(this, "border" + tl + "Width") +
			num(this, "border" + br + "Width") +
			(margin ?
				num(this, "margin" + tl) + num(this, "margin" + br) : 0);
	};
	
	var type = name.toLowerCase();

	jQuery.fn[ type ] = function( size ) {
		// Get window width or height
		return this[0] == window ?
			// Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
			document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] ||
			document.body[ "client" + name ] :

			// Get document width or height
			this[0] == document ?
				// Either scroll[Width/Height] or offset[Width/Height], whichever is greater
				Math.max(
					document.documentElement["client" + name],
					document.body["scroll" + name], document.documentElement["scroll" + name],
					document.body["offset" + name], document.documentElement["offset" + name]
				) :

				// Get or set width or height on the element
				size === undefined ?
					// Get width or height on the element
					(this.length ? jQuery.css( this[0], type ) : null) :

					// Set the width or height on the element (default to pixels if value is unitless)
					this.css( type, typeof size === "string" ? size : size + "px" );
	};

});})();

/*
 * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/
 *
 * Uses the built in easing capabilities added In jQuery 1.1
 * to offer multiple easing options
 *
 * TERMS OF USE - jQuery Easing
 * 
 * Open source under the BSD License. 
 * 
 * Copyright Ã‚Â© 2008 George McGinley Smith
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of 
 * conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this list 
 * of conditions and the following disclaimer in the documentation and/or other materials 
 * provided with the distribution.
 * 
 * Neither the name of the author nor the names of contributors may be used to endorse 
 * or promote products derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 *  GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE. 
 *
*/

// t: current time, b: begInnIng value, c: change In value, d: duration
jQuery.easing['jswing'] = jQuery.easing['swing'];

jQuery.extend( jQuery.easing,
{
	def: 'easeOutQuad',
	swing: function (x, t, b, c, d) {
		//alert(jQuery.easing.default);
		return jQuery.easing[jQuery.easing.def](x, t, b, c, d);
	},
	easeInQuad: function (x, t, b, c, d) {
		return c*(t/=d)*t + b;
	},
	easeOutQuad: function (x, t, b, c, d) {
		return -c *(t/=d)*(t-2) + b;
	},
	easeInOutQuad: function (x, t, b, c, d) {
		if ((t/=d/2) < 1) return c/2*t*t + b;
		return -c/2 * ((--t)*(t-2) - 1) + b;
	},
	easeInCubic: function (x, t, b, c, d) {
		return c*(t/=d)*t*t + b;
	},
	easeOutCubic: function (x, t, b, c, d) {
		return c*((t=t/d-1)*t*t + 1) + b;
	},
	easeInOutCubic: function (x, t, b, c, d) {
		if ((t/=d/2) < 1) return c/2*t*t*t + b;
		return c/2*((t-=2)*t*t + 2) + b;
	},
	easeInQuart: function (x, t, b, c, d) {
		return c*(t/=d)*t*t*t + b;
	},
	easeOutQuart: function (x, t, b, c, d) {
		return -c * ((t=t/d-1)*t*t*t - 1) + b;
	},
	easeInOutQuart: function (x, t, b, c, d) {
		if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
		return -c/2 * ((t-=2)*t*t*t - 2) + b;
	},
	easeInQuint: function (x, t, b, c, d) {
		return c*(t/=d)*t*t*t*t + b;
	},
	easeOutQuint: function (x, t, b, c, d) {
		return c*((t=t/d-1)*t*t*t*t + 1) + b;
	},
	easeInOutQuint: function (x, t, b, c, d) {
		if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
		return c/2*((t-=2)*t*t*t*t + 2) + b;
	},
	easeInSine: function (x, t, b, c, d) {
		return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
	},
	easeOutSine: function (x, t, b, c, d) {
		return c * Math.sin(t/d * (Math.PI/2)) + b;
	},
	easeInOutSine: function (x, t, b, c, d) {
		return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
	},
	easeInExpo: function (x, t, b, c, d) {
		return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
	},
	easeOutExpo: function (x, t, b, c, d) {
		return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
	},
	easeInOutExpo: function (x, t, b, c, d) {
		if (t==0) return b;
		if (t==d) return b+c;
		if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
		return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
	},
	easeInCirc: function (x, t, b, c, d) {
		return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
	},
	easeOutCirc: function (x, t, b, c, d) {
		return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
	},
	easeInOutCirc: function (x, t, b, c, d) {
		if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
		return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
	},
	easeInElastic: function (x, t, b, c, d) {
		var s=1.70158;var p=0;var a=c;
		if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
		if (a < Math.abs(c)) { a=c; var s=p/4; }
		else var s = p/(2*Math.PI) * Math.asin (c/a);
		return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
	},
	easeOutElastic: function (x, t, b, c, d) {
		var s=1.70158;var p=0;var a=c;
		if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
		if (a < Math.abs(c)) { a=c; var s=p/4; }
		else var s = p/(2*Math.PI) * Math.asin (c/a);
		return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
	},
	easeInOutElastic: function (x, t, b, c, d) {
		var s=1.70158;var p=0;var a=c;
		if (t==0) return b;  if ((t/=d/2)==2) return b+c;  if (!p) p=d*(.3*1.5);
		if (a < Math.abs(c)) { a=c; var s=p/4; }
		else var s = p/(2*Math.PI) * Math.asin (c/a);
		if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
		return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
	},
	easeInBack: function (x, t, b, c, d, s) {
		if (s == undefined) s = 1.70158;
		return c*(t/=d)*t*((s+1)*t - s) + b;
	},
	easeOutBack: function (x, t, b, c, d, s) {
		if (s == undefined) s = 1.70158;
		return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
	},
	easeInOutBack: function (x, t, b, c, d, s) {
		if (s == undefined) s = 1.70158; 
		if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
		return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
	},
	easeInBounce: function (x, t, b, c, d) {
		return c - jQuery.easing.easeOutBounce (x, d-t, 0, c, d) + b;
	},
	easeOutBounce: function (x, t, b, c, d) {
		if ((t/=d) < (1/2.75)) {
			return c*(7.5625*t*t) + b;
		} else if (t < (2/2.75)) {
			return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
		} else if (t < (2.5/2.75)) {
			return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
		} else {
			return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
		}
	},
	easeInOutBounce: function (x, t, b, c, d) {
		if (t < d/2) return jQuery.easing.easeInBounce (x, t*2, 0, c, d) * .5 + b;
		return jQuery.easing.easeOutBounce (x, t*2-d, 0, c, d) * .5 + c*.5 + b;
	}
});

/*
 *
 * TERMS OF USE - EASING EQUATIONS
 * 
 * Open source under the BSD License. 
 * 
 * Copyright Ã‚Â© 2001 Robert Penner
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of 
 * conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this list 
 * of conditions and the following disclaimer in the documentation and/or other materials 
 * provided with the distribution.
 * 
 * Neither the name of the author nor the names of contributors may be used to endorse 
 * or promote products derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 *  GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE. 
 *
 */

/*
Copyright (c) 2009 Ronnie Garcia, Travis Nickels

This file is part of Uploadify v1.6.2

Permission is hereby granted, free of charge, to any person obtaining a copy
of Uploadify and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

UPLOADIFY IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

var flashVer = "-1.";
if (navigator.plugins != null && navigator.plugins.length > 0) {
	if (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]) {
		var swVer2 = navigator.plugins["Shockwave Flash 2.0"] ? " 2.0" : "";
		var flashDescription = navigator.plugins["Shockwave Flash" + swVer2].description;
		var descArray = flashDescription.split(" ");
		var tempArrayMajor = descArray[2].split(".");			
		var versionMajor = tempArrayMajor[0];
		var versionMinor = tempArrayMajor[1];
		var versionRevision = descArray[3];
		if (versionRevision == "") {
			versionRevision = descArray[4];
		}
		if (versionRevision[0] == "d") {
			versionRevision = versionRevision.substring(1);
		} else if (versionRevision[0] == "r") {
			ersionRevision = versionRevision.substring(1);
			if (versionRevision.indexOf("d") > 0) {
				versionRevision = versionRevision.substring(0, versionRevision.indexOf("d"));
			}
		}
		var flashVer = versionMajor + "." + versionMinor	 + "." + versionRevision;
	}
} else if ( $.browser.msie ) {
	var version;
	var axo;
	var e;
	try {
		axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
		version = axo.GetVariable("$version");
	} catch (e) {
	}	
	flashVer = version.replace("WIN ","").replace(",",".");
}
flashVer = flashVer.split(".")[0];

if(jQuery)(
	function($){
		$.extend($.fn,{
			fileUpload:function(options) {
				if (flashVer >= 9) {
					$(this).each(function(){
						settings = $.extend({
						uploader:      'uploader.swf',
						script:        'uploader.php',
						folder:        '',
						height:        30,
						width:         110,
						cancelImg:     'cancel.png',
						wmode:         'opaque',
						scriptAccess:  'sameDomain',
						fileDataName:  'Filedata',
						displayData:   'percentage',
						onInit:        function() {},
						onSelect:      function() {},
						onCheck:       function() {},
						onCancel:      function() {},
						onError:       function() {},
						onProgress:    function() {},
						onComplete:    function() {}
					}, options);
					var pagePath = location.pathname;
					pagePath = pagePath.split('/');
					pagePath.pop();
					pagePath = pagePath.join('/') + '/';
					var data = '&pagepath=' + pagePath;
					if (settings.buttonImg) data += '&buttonImg=' + escape(settings.buttonImg);
					if (settings.buttonText) data += '&buttonText=' + escape(settings.buttonText);
					if (settings.rollover) data += '&rollover=true';
					data += '&script=' + settings.script;
          data += '&folder=' + escape(settings.folder);
					if (settings.scriptData) {
						var scriptDataString = '';
						for (var name in settings.scriptData) {
							scriptDataString += '&' + name + '=' + settings.scriptData[name];
						}
						data += '&scriptData=' + escape(scriptDataString);
					}
					data += '&btnWidth=' + settings.width;
					data += '&btnHeight=' + settings.height;
					data += '&wmode=' + settings.wmode;
					if (settings.hideButton) data += '&hideButton=true';
					if (settings.fileDesc) data += '&fileDesc=' + settings.fileDesc + '&fileExt=' + settings.fileExt;
					if (settings.multi) data += '&multi=true';
					if (settings.auto) data += '&auto=true';
					if (settings.sizeLimit) data += '&sizeLimit=' + settings.sizeLimit;
					if (settings.simUploadLimit) data += '&simUploadLimit=' + settings.simUploadLimit;
					if (settings.checkScript) data += '&checkScript=' + settings.checkScript;
					if (settings.fileDataName) data += '&fileDataName=' + settings.fileDataName;
					if ($.browser.msie) {
						flashElement = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="' + settings.width + '" height="' + settings.height + '" id="' + $(this).attr("id")  + 'Uploader" class="fileUploaderBtn">\
						<param name="movie" value="' + settings.uploader + '?fileUploadID=' + $(this).attr("id") + data + '" />\
						<param name="quality" value="high" />\
						<param name="wmode" value="' + settings.wmode + '" />\
						<param name="allowScriptAccess" value="' + settings.scriptAccess + '">\
						<param name="swfversion" value="9.0.0.0" />\
						</object>';
					} else {
						flashElement = '<embed src="' + settings.uploader + '?fileUploadID=' + $(this).attr("id") + data + '" quality="high" width="' + settings.width + '" height="' + settings.height + '" id="' + $(this).attr("id") + 'Uploader" class="fileUploaderBtn" name="' + $(this).attr("id") + 'Uploader" allowScriptAccess="' + settings.scriptAccess + '" wmode="' + settings.wmode + '" type="application/x-shockwave-flash" />';
					}
					if (settings.onInit() !== false) {
						$(this).css('display','none');
						if ($.browser.msie) {
							$(this).after('<div id="' + $(this).attr("id")  + 'Uploader"></div>');
							document.getElementById($(this).attr("id")  + 'Uploader').outerHTML = flashElement;
						} else {
							$(this).after(flashElement);
						}
						$("#" + $(this).attr('id') + "Uploader").after('<div id="' + $(this).attr('id') + 'Queue" class="fileUploadQueue"></div>');
					}
					$(this).bind("rfuSelect", {'action': settings.onSelect}, function(event, queueID, fileObj) {
						if (event.data.action(event, queueID, fileObj) !== false) {
							var byteSize = Math.round(fileObj.size / 1024 * 100) * .01;
							var suffix = 'KB';
							if (byteSize > 1000) {
								byteSize = Math.round(byteSize *.001 * 100) * .01;
								suffix = 'MB';
							}
							var sizeParts = byteSize.toString().split('.');
							if (sizeParts.length > 1) {
								byteSize = sizeParts[0] + '.' + sizeParts[1].substr(0,2);
							} else {
								byteSize = sizeParts[0];
							}
							if (fileObj.name.length > 20) {
								fileName = fileObj.name.substr(0,20) + '...';
							} else {
								fileName = fileObj.name;
							}
							$('#' + $(this).attr('id') + 'Queue').append('<div id="' + $(this).attr('id') + queueID + '" class="fileUploadQueueItem">\
									<div class="cancel">\
										<a href="javascript:$(\'#' + $(this).attr('id') + '\').fileUploadCancel(\'' + queueID + '\')"><img src="' + settings.cancelImg + '" border="0" /></a>\
									</div>\
									<span class="fileName">' + fileName + ' (' + byteSize + suffix + ')</span><span class="percentage">&nbsp;</span>\
									<div class="fileUploadProgress" style="width: 100%;">\
										<div id="' + $(this).attr('id') + queueID + 'ProgressBar" class="fileUploadProgressBar" style="width: 1px; height: 3px;"></div>\
									</div>\
								</div>');
						}
					});
					if (typeof(settings.onSelectOnce) == 'function') {
						$(this).bind("rfuSelectOnce", settings.onSelectOnce);
					}
					$(this).bind("rfuCheckExist", {'action': settings.onCheck}, function(event, checkScript, fileQueue, folder, single) {
						var postData = new Object();
						postData.folder = pagePath + folder;
						for (var queueID in fileQueue) {
							postData[queueID] = fileQueue[queueID];
							if (single) {
								var singleFileID = queueID;
							}
						}
						$.post(checkScript, postData, function(data) {
							for(var key in data) {
								if (event.data.action(event, checkScript, fileQueue, folder, single) !== false) {
									var replaceFile = confirm('Do you want to replace the file \'' + data[key] + '\'?');
									if (!replaceFile) {
										document.getElementById($(event.target).attr('id') + 'Uploader').cancelFileUpload(key);
									}
								}
							}
							if (single) {
								document.getElementById($(event.target).attr('id') + 'Uploader').startFileUpload(singleFileID, true);
							} else {
								document.getElementById($(event.target).attr('id') + 'Uploader').startFileUpload(null, true);
							}
						}, "json");
					});
					$(this).bind("rfuCancel", {'action': settings.onCancel}, function(event, queueID, fileObj, data) {
						if (event.data.action(event, queueID, fileObj, data) !== false) {
							$("#" + $(this).attr('id') + queueID).fadeOut(250, function() { $("#" + $(this).attr('id') + queueID).remove()});
						}
					});
					$(this).bind("rfuClearQueue", {'action': settings.onClearQueue}, function() {
						if (event.data.action() !== false) {
							$('#' + $(this).attr('id') + 'Queue').contents().fadeOut(250, function() {$('#' + $(this).attr('id') + 'Queue').empty()});
						}
					});
					$(this).bind("rfuError", {'action': settings.onError}, function(event, queueID, fileObj, errorObj) {
						if (event.data.action(event, queueID, fileObj, errorObj) !== false) {
							$("#" + $(this).attr('id') + queueID + " .fileName").text(errorObj.type + " Error - " + fileObj.name);
							$("#" + $(this).attr('id') + queueID).css({'border': '3px solid #FBCBBC', 'background-color': '#FDE5DD'});
						}
					});
					$(this).bind("rfuProgress", {'action': settings.onProgress, 'toDisplay': settings.displayData}, function(event, queueID, fileObj, data) {
						if (event.data.action(event, queueID, fileObj, data) !== false) {
							$("#" + $(this).attr('id') + queueID + "ProgressBar").css('width', data.percentage + '%');
							if (event.data.toDisplay == 'percentage') displayData = ' - ' + data.percentage + '%';
							if (event.data.toDisplay == 'speed') displayData = ' - ' + data.speed + 'KB/s';
							if (event.data.toDisplay == null) displayData = ' ';
							$("#" + $(this).attr('id') + queueID + " .percentage").text(displayData);
						}
					});
					$(this).bind("rfuComplete", {'action': settings.onComplete}, function(event, queueID, fileObj, response, data) {
						if (event.data.action(event, queueID, fileObj, unescape(response), data) !== false) {
							$("#" + $(this).attr('id') + queueID).fadeOut(250, function() { $("#" + $(this).attr('id') + queueID).remove()});
							$("#" + $(this).attr('id') + queueID + " .percentage").text(' - Completed');
						}
					});
					if (typeof(settings.onAllComplete) == 'function') {
						$(this).bind("rfuAllComplete", settings.onAllComplete);
					}
					if (typeof(settings.onStart) == 'function') {
						$(this).bind("rfuStart", settings.onStart);
					}
				});
			}
		},
		fileUploadSettings:function(settingName, settingValue) {
			$(this).each(function() {
				document.getElementById($(this).attr('id') + 'Uploader').updateSettings(settingName,settingValue);
			});
		},
		fileUploadStart:function(queueID) {
			$(this).each(function() {
				document.getElementById($(this).attr('id') + 'Uploader').startFileUpload(queueID, false);
			});
		},
		fileUploadCancel:function(queueID) {
			$(this).each(function() {
				document.getElementById($(this).attr('id') + 'Uploader').cancelFileUpload(queueID);
			});
		},
		fileUploadClearQueue:function() {
			$(this).each(function() {
				document.getElementById($(this).attr('id') + 'Uploader').clearFileUploadQueue();
			});
		}
	})
})(jQuery);

/*******************************************************************************
 * dPassword v0.1 - jQuery delayed password masking (iPhone style)
 *
 * licensed under MIT License
 *
 * Copyright (c) 2009 DECAF°, Stefan Ullrich (http://decaf.de)
 *
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be 
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 
 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Known Issues: - delete marked text will not work
 *               - deleting a single chars will not work if char is 
 *                 not the last char
 *               - view will not follow cursor if textfield is too small
 *               - if id based styles are assigned, these styles will 
 *                 not be taken over
 *
 *******************************************************************************/

(function($){$.fn.dPassword=function(options){var defaults={interval:200,duration:3000,replacement:'%u25CF',prefix:'password_',debug:false}
var opts=$.extend(defaults,options);var checker=new Array();var timer=new Array();$(this).each(function(){if(opts.debug)console.log('init ['+$(this).attr('id')+']');var name=$(this).attr('name');var id=$(this).attr('id');var cssclass=$(this).attr('class');var style=$(this).attr('style');var size=$(this).attr('size');var maxlength=$(this).attr('maxlength');var disabled=$(this).attr('disabled');var tabindex=$(this).attr('tabindex');var accesskey=$(this).attr('accesskey');var value=$(this).attr('value');checker.push(id);timer.push(id);$(this).hide();if(opts.debug){$(this).after('<span id="debug_'+opts.prefix+name+'" style="color: #f00;"></span>');}
$(this).after(' <input name="'+(opts.prefix+name)+'" '+'id="'+(opts.prefix+id)+'" '+'type="text" '+'value="'+value+'" '+
(cssclass!=''?'class="'+cssclass+'"':'')+
(style!=''?'style="'+style+'"':'')+
(size!=''?'size="'+size+'"':'')+
(maxlength!=-1?'maxlength="'+maxlength+'"':'')+
(disabled!=''?'disabled="'+disabled+'"':'')+
(tabindex!=''?'tabindex="'+tabindex+'"':'')+
(accesskey!=undefined?'accesskey="'+accesskey+'"':'')+'autocomplete="off" />');$('label[for='+id+']').attr('for',opts.prefix+id);$(this).attr('tabindex','');$(this).attr('accesskey','');$('#'+opts.prefix+id).bind('focus',function(event){if(opts.debug)console.log('event: focus ['+getId($(this).attr('id'))+']');clearTimeout(checker[getId($(this).attr('id'))]);checker[getId($(this).attr('id'))]=setTimeout("check('"+getId($(this).attr('id'))+"', '')",opts.interval);});$('#'+opts.prefix+id).bind('blur',function(event){if(opts.debug)console.log('event: blur ['+getId($(this).attr('id'))+']');clearTimeout(checker[getId($(this).attr('id'))]);});setTimeout("check('"+id+"', '', true);",opts.interval);});getId=function(id){var pattern=opts.prefix+'(.*)';var regex=new RegExp(pattern);regex.exec(id);id=RegExp.$1;return id;}
setPassword=function(id,str){if(opts.debug)console.log('setPassword: ['+id+']');var tmp='';for(i=0;i<str.length;i++){if(str.charAt(i)==unescape(opts.replacement)){tmp=tmp+$('#'+id).val().charAt(i);}
else{tmp=tmp+str.charAt(i);}}
$('#'+id).val(tmp);}
check=function(id,oldValue,initialCall){if(opts.debug)console.log('check: ['+id+']');var bullets=$('#'+opts.prefix+id).val();if(oldValue!=bullets){setPassword(id,bullets);if(bullets.length>1){var tmp='';for(i=0;i<bullets.length-1;i++){tmp=tmp+unescape(opts.replacement);}
tmp=tmp+bullets.charAt(bullets.length-1);$('#'+opts.prefix+id).val(tmp);}
else{}
clearTimeout(timer[id]);timer[id]=setTimeout("convertLastChar('"+id+"')",opts.duration);}
if(opts.debug){$('#debug_'+opts.prefix+id).text($('#'+id).val());}
if(!initialCall){checker[id]=setTimeout("check('"+id+"', '"+$('#'+opts.prefix+id).val()+"', false)",opts.interval);}}
convertLastChar=function(id){if($('#'+opts.prefix+id).val()!=''){var tmp='';for(i=0;i<$('#'+opts.prefix+id).val().length;i++){tmp=tmp+unescape(opts.replacement);}
$('#'+opts.prefix+id).val(tmp);}}};})(jQuery);

// vim: ts=4:sw=4:nu:fdc=2:nospell
/**
 * Ext.ux.HttpProvider extension
 *
 * @author    Ing. Jozef Sakalos
 * @copyright (c) 2008, Ing. Jozef Sakalos
 * @version $Id: Ext.ux.HttpProvider.js 481 2009-01-23 22:10:18Z jozo $
 *
 * @license Ext.ux.HttpProvider is licensed under the terms of
 * the Open Source LGPL 3.0 license.  Commercial use is permitted to the extent
 * that the code/component(s) do NOT become part of another Open Source or Commercially
 * licensed development library or toolkit without explicit permission.
 * 
 * License details: http://www.gnu.org/licenses/lgpl.html
 */

/*global Ext, console */

// {{{
// Define clone function if it is not already defined
if('function' !== Ext.type(Ext.ux.clone)) {
    Ext.ux.clone = function(o) {
        if('object' !== typeof o) {
            return o;
        }
        var c = '[object Array]' === Object.prototype.toString.call(o) ? [] : {};
        var p, v;
        for(p in o) {
            if(o.hasOwnProperty(p)) {
                v = o[p];
                if('object' === typeof v) {
                    c[p] = Ext.ux.clone(v);
                }
                else {
                    c[p] = v;
                }
            }
        }
        return c;
    };
} // eo clone
// }}}

/**
 * @class Ext.ux.HttpProvider
 * @extends Ext.state.Provider
 * @constructor
 * @param {Object} config Configuration object
 */
// {{{
Ext.ux.HttpProvider = function(config) {

    this.addEvents(
        /**
         * @event readsuccess
         * Fires after state has been successfully received from server and restored
         * @param {HttpProvider} this
         */
         'readsuccess'
        /**
         * @event readfailure
         * Fires in the case of an error when attempting to read state from server
         * @param {HttpProvider} this
         */
        ,'readfailure'
        /**
         * @event savesuccess
         * Fires after the state has been successfully saved to server
         * @param {HttpProvider} this
         */
        ,'savesuccess'
        /**
         * @event savefailure
         * Fires in the case of an error when attempting to save state to the server
         * @param {HttpProvider} this
         */
        ,'savefailure'
    );

    // call parent 
    Ext.ux.HttpProvider.superclass.constructor.call(this);

    Ext.apply(this, config, {
        // defaults
         delay:750 // buffer changes for 750 ms
        ,dirty:false
        ,started:false
        ,autoStart:true
        ,autoRead:true
        ,user:'user'
        ,id:1
        ,session:'session'
        ,logFailure:false
        ,logSuccess:false
        ,queue:[]
        ,url:'.'
        ,readUrl:undefined
        ,saveUrl:undefined
        ,method:'post'
        ,saveBaseParams:{}
        ,readBaseParams:{}
        ,paramNames:{
             id:'id'
            ,name:'name'
            ,value:'value'
            ,user:'user'
            ,session:'session'
            ,data:'data'
        }
    }); // eo apply

    if(this.autoRead) {
        this.readState();
    }

    this.dt = new Ext.util.DelayedTask(this.submitState, this);
    if(this.autoStart) {
        this.start();
    }
}; // eo constructor
// }}}

Ext.extend(Ext.ux.HttpProvider, Ext.state.Provider, {

    // localizable texts
     saveSuccessText:'Save Success'
    ,saveFailureText:'Save Failure'
    ,readSuccessText:'Read Success'
    ,readFailureText:'Read Failure'
    ,dataErrorText:'Data Error'

    // {{{
    /**
     * Initializes state from the passed state object or array.
     * This method can be called early during page load having the state Array/Object
     * retrieved from database by server.
     * @param {Array/Object} state State to initialize state manager with
     */
    ,initState:function(state) {
        if(state instanceof Array) {
            Ext.each(state, function(item) {
                this.state[item.name] = this.decodeValue(item[this.paramNames.value]);
            }, this);
        }
        else {
            this.state = state ? state : {};
        }
    } // eo function initState
    // }}}
    // {{{
    /**
     * Sets the passed state variable name to the passed value and queues the change
     * @param {String} name Name of the state variable
     * @param {Mixed} value Value of the state variable
     */
    ,set:function(name, value) {
        if(!name) {
            return;
        }

        this.queueChange(name, value);

    } // eo function set
    // }}}
    // {{{
    /**
     * Starts submitting state changes to server
     */
    ,start:function() {
        this.dt.delay(this.delay);
        this.started = true;
    } // eo function start
    // }}}
    // {{{
    /**
     * Stops submitting state changes
     */
    ,stop:function() {
        this.dt.cancel();
        this.started = false;
    } // eo function stop
    // }}}
    // {{{
    /**
     * private, queues the state change if state has changed
     */
    ,queueChange:function(name, value) {
        var changed = undefined === this.state[name] || this.state[name] !== value;
        var o = {};
        var i;
        var found = false;
        if(changed) {
            o[this.paramNames.name] = name;
            o[this.paramNames.value] = this.encodeValue(value);
            for(i = 0; i < this.queue.length; i++) {
                if(this.queue[i].name === o.name) {
                    this.queue[i] = o;
                    found = true;
                }
            }
            if(false === found) {
                this.queue.push(o);
            }
            this.dirty = true;
        }
        return changed;
    } // eo function bufferChange
    // }}}
    // {{{
    /**
     * private, submits state to server by asynchronous Ajax request
     */
    ,submitState:function() {
        if(!this.dirty) {
            this.dt.delay(this.delay);
            return;
        }
        this.dt.cancel();

        var o = {
             url:this.saveUrl || this.url
            ,method:this.method
            ,scope:this
            ,success:this.onSaveSuccess
            ,failure:this.onSaveFailure
            ,queue:Ext.ux.clone(this.queue)
            ,params:{}
        };

        var params = Ext.apply({}, this.saveBaseParams);
        params[this.paramNames.id] = this.id;
        params[this.paramNames.user] = this.user;
        params[this.paramNames.session] = this.session;
        params[this.paramNames.data] = Ext.encode(o.queue);

        Ext.apply(o.params, params);

        // be optimistic
        this.dirty = false;

        Ext.Ajax.request(o);
    } // eo function submitState
    // }}}
    // {{{
    /**
     * Clears the state variable
     * @param {String} name Name of the variable to clear
     */
    ,clear:function(name) {
        this.set(name, undefined);
    } // eo function clear
    // }}}
    // {{{
    /**
     * private, save success callback
     */
    ,onSaveSuccess:function(response, options) {
        if(this.started) {
            this.start();
        }
        var o = {};
        try {o = Ext.decode(response.responseText);}
        catch(e) {
            if(true === this.logFailure) {
                this.log(this.saveFailureText, e, response);
            }
            this.dirty = true;
            return;
        }
        if(true !== o.success) {
            if(true === this.logFailure) {
                this.log(this.saveFailureText, o, response);
            }
            this.dirty = true;
        }
        else {
            Ext.each(options.queue, function(item) {
                if(!item) {
                    return;
                }
                var name = item[this.paramNames.name];
                var value = this.decodeValue(item[this.paramNames.value]);

                if(undefined === value || null === value) {
                    Ext.ux.HttpProvider.superclass.clear.call(this, name);
                }
                else {
                    // parent sets value and fires event
                    Ext.ux.HttpProvider.superclass.set.call(this, name, value);
                }
            }, this);
            if(false === this.dirty) {
                this.queue = [];
            }
            else {
                var i, j, found;
                for(i = 0; i < options.queue.length; i++) {
                    found = false;
                    for(j = 0; j < this.queue.length; j++) {
                        if(options.queue[i].name === this.queue[j].name) {
                            found = true;
                            break;
                        }
                    }
                    if(true === found && this.encodeValue(options.queue[i].value) === this.encodeValue(this.queue[j].value)) {
                        this.queue.remove(this.queue[j]);
                    }
                }
            }
            if(true === this.logSuccess) {
                this.log(this.saveSuccessText, o, response);
            }
            this.fireEvent('savesuccess', this);
        }
    } // eo function onSaveSuccess
    // }}}
    // {{{
    /**
     * private, save failure callback
     */
    ,onSaveFailure:function(response, options) {
        if(true === this.logFailure) {
            this.log(this.saveFailureText, response);
        }
        if(this.started) {
            this.start();
        }
        this.dirty = true;
        this.fireEvent('savefailure', this);
    } // eo function onSaveFailure
    // }}}
    // {{{
    /**
     * private, read state callback
     */
    ,onReadFailure:function(response, options) {
        if(true === this.logFailure) {
            this.log(this.readFailureText, response);
        }
        this.fireEvent('readfailure', this);

    } // eo function onReadFailure
    // }}}
    // {{{
    /**
     * private, read success callback
     */
    ,onReadSuccess:function(response, options) {
        var o = {}, data;
        try {o = Ext.decode(response.responseText);}
        catch(e) {
            if(true === this.logFailure) {
                this.log(this.readFailureText, e, response);
            }
            return;
        }
        if(true !== o.success) {
            if(true === this.logFailure) {
                this.log(this.readFailureText, o, response);
            }
        }
        else {
            data = o[this.paramNames.data];
            if(!(data instanceof Array) && true === this.logFailure) {
                this.log(this.dataErrorText, data, response);
                return;
            }
            Ext.each(data, function(item) {
                this.state[item[this.paramNames.name]] = this.decodeValue(item[this.paramNames.value]);
            }, this);
            this.queue = [];
            this.dirty = false;
            if(true === this.logSuccess) {
                this.log(this.readSuccessText, data, response);
            }
            this.fireEvent('readsuccess', this);
        }
    } // eo function onReadSuccess
    // }}}
    // {{{
    /**
     * Reads saved state from server by sending asynchronous Ajax request and processing the response
     */
    ,readState:function() {
        var o = {
             url:this.readUrl || this.url
            ,method:this.method
            ,scope:this
            ,success:this.onReadSuccess
            ,failure:this.onReadFailure
            ,params:{}
        };

        var params = Ext.apply({}, this.readBaseParams);
        params[this.paramNames.id] = this.id;
        params[this.paramNames.user] = this.user;
        params[this.paramNames.session] = this.session;

        Ext.apply(o.params, params);
        Ext.Ajax.request(o);
    } // eo function readState
    // }}}
    // {{{
    /**
     * private, logs errors or successes
     */
    ,log:function() {
        if(console) {
            console.log.apply(console, arguments);
        }
    } // eo log
    // }}}

}); // eo extend

// eof

if (typeof Kohive == "undefined") {
  Ext.ns('Kohive');
};

/**
 * @class Kohive.HttpProvider
 * @extends Ext.ux.HttpProvider
 * Extends the HttpProvider so we can use it with our PHP backend.
 */
Kohive.HttpProvider = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    url:        '/states',
    autoRead:   false,
    delay:      750,
    queue:      {},
    sendMethod: 'POST',
    readMethod: 'GET'
  });
  
  Kohive.HttpProvider.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.HttpProvider, Ext.ux.HttpProvider, {
  
  /**
   * Initalizes the Kohive HTTTP state provider by sending off a request to the
   * server and setting this.state
   */
  initState: function() {
    this.state = {};
    
    // Success function
    var successFn = function(response) {
      var o = {};
      
      // Try to decode the response text and log out the error if set
      try {
        o = Ext.decode(response.responseText);
      } catch(e) {
        if (true === this.logFailure) {
          this.log(this.readFailureText, e, response);
        };
        return;
      };
      
      // Loop through each of the items in response
      for (var i = 0; i < o.length; i++) {
        var n = o[i].key;
        var v = o[i].data;
        
        // Decode the value and add it into the state config
        this.state[n] = this.decodeValue(v);
      };
    };
    
    // Failure function
    var failureFn = function(response) {
      if (true === this.logFailure) {
        this.log(this.readFailureText, e, response);
      };
    };
    
    var o = {
      url:     this.readUrl   || this.url,
      method:  this.getMethod || 'GET',
      scope:   this,
      success: successFn,
      failure: failureFn
    };
    
    // Send off the request
    Ext.Ajax.request(o);
  },
  
  // private
  queueChange: function(name, value) {
    var changed = undefined === this.state[name] || this.state[name] !== value;
    var o = {};
    var i;
    var found = false;
    if(changed) {
      var n = name;
      var v = this.encodeValue(value);
      
      if(this.queue[n]) {
        this.queue[n] = v;
        found = true;
      }
      if(false === found) {
        this.queue[name] = this.encodeValue(value);
      }
      this.dirty = true;
    }
    return changed;
  },
  
  // private
  submitState: function() {
    if(!this.dirty) {
      this.dt.delay(this.delay);
      return;
    }
    this.dt.cancel();
    
    var o = {
      url:     this.saveUrl    || this.url,
      method:  this.sendMethod || 'POST',
      scope:   this,
      success: this.onSaveSuccess,
      failure: this.onSaveFailure,
      queue:   Ext.ux.clone(this.queue),
      params:  {}
    };
    
    for (r in o.queue) {
      Ext.apply(o.params, {
        key:  r,
        data: o.queue[r]
      });
    };
    
    // be optimistic
    this.dirty = false;
    
    Ext.Ajax.request(o);
  },
  
  onSaveSuccess: function(response, options) {
    if(this.started) {
      this.start();
    };
    
    this.fireEvent('savesuccess', this);
  },
  
  // private
  readState: function() {
    
    var o = {
      url:     this.readUrl   || this.url,
      method:  this.getMethod || 'GET',
      scope:   this,
      success: this.onReadSuccess,
      failure: this.onReadFailure
    };
    
    Ext.Ajax.request(o);
  },
  
  onReadSuccess: function(response, options) {
    var o = {};
    try {
      o = Ext.decode(response.responseText);
    } catch(e) {
      if (true === this.logFailure) {
        this.log(this.readFailureText, e, response);
      };
      return;
    };
    
    for (item in o) {
      var n = item;
      var v = o[item];
      
      this.state[n] = v;
    };
    
    // this.queue = [];
    this.dirty = false;
    
    if (true === this.logSuccess) {
      this.log(this.readSuccessText, data, response);
    };
    
    this.fireEvent('readsuccess', this);
  }
  
});

/**
 * Add site-wide settings such as BLANK_IMAGE_URL
 */

Ext.BLANK_IMAGE_URL = 'images/default/s.gif';

// Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
Ext.state.Manager.setProvider(new Kohive.HttpProvider());
Ext.state.Manager.getProvider().initState();

if (!window.console) console = {log: function() {}};

Ext.onReady(function() {
  Ext.QuickTips.init();
	
	setTimeout(function() {
		if (!Ext.get('loading-mask')) return;
    Ext.get('loading-mask').fadeOut({remove:true});
    Ext.get('loading').fadeOut({remove:true});
    Ext.get('loading-icon').fadeOut({remove:true});
  }, 250);
	
  // //init tinymce
  // tinyMCE.init({
  //  mode : "none",
  //  theme : "advanced",
  //  
  //  theme_advanced_toolbar_location:  "top",
  //  theme_advanced_toolbar_align:     "left",
  //  theme_advanced_resizing:          true,
  //  theme_advanced_resize_horizontal: true,
  //  
  //  plugins : "autoresize,safari,advhr,advimage,advlink,print,inlinepopups,directionality,visualchars,nonbreaking,xhtmlxtras,template",
  // 
  //  // Theme options
  //  theme_advanced_buttons1 : "undo,redo,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,hr,|,outdent,indent,blockquote,|,formatselect",
  //  // theme_advanced_buttons2 : "bullist,numlist,|,outdent,indent,blockquote,|,link,unlink,|,image,|,code",
  //  // theme_advanced_buttons2 : "outdent,indent,blockquote",
  //  theme_advanced_buttons2 : "",
  //  theme_advanced_buttons3 : ""
  // });
	
  Kohive.OS.launch();
});

/**
 * Defines which database adapter to use in development, production and test
 */

// Set the ExtMVC.model default adaptor
// ExtMVC.Model.prototype.adapter = "REST";



// Set the default extension for all requests
// ExtMVC.Model.Adapter.REST.classMethods.urlExtension = '.json';

// ExtMVC.UrlBuilder.baseUrlExtension = '.json';

/**
 * Defines all routes required for this application
 */
ExtMVC.Router.defineRoutes = function(map) {
  /**
   * Sets up REST-like urls for a given model or models:
   * 
   * map.resources('users');
   * 
   * Is equivalent to:
   * map.connect("users",           {controller: 'users', action: 'index'}); // #users
   * map.connect("users/new",       {controller: 'users', action: 'new'  }); // #users/new
   * map.connect("users/:id/edit/", {controller: 'users', action: 'edit' }); // #users/1/edit
   * map.connect("users/:id",       {controller: 'users', action: 'show' }); // #users/1
   * 
   * You can pass more than one model to a map.resources call, e.g.:
   *
   * map.resources('users', 'comments', 'pages', 'products');
   */
  
	// Setup the fake route for messageboard + chatroom
	map.connect("messageboard", {controller: 'boardposts', action: 'index'});
	map.connect("chatroom",     {controller: 'chat',       action: 'index'});
	map.connect("blogs",        {controller: 'blogs',      action: 'index'});
	map.connect("blog",         {controller: 'blogs',      action: 'index'});

  map.resources(
    'blog',
    'bookmarks',
    'boardposts',
    'doodle',
    'filecabinet',
    'idcards',
    'invite',
    'photos',
    'microblogs',
    'overview',
    'settings',
    'stickies',
    'tasks',
    'videos'
  );
  
  //set up default routes
  map.connect(":controller/:action");
  map.connect(":controller/:action/:id");
  
  //if no url, should a default
  map.root({controller: 'index', action: 'index'});
};

Ext.isArray = function(obj) {
  if (!obj) { return; };
  
  return obj.constructor == Array;
};

Array.prototype.compareArrays = function(arr) {
    if (this.length != arr.length) return false;
    for (var i = 0; i < arr.length; i++) {
        if (this[i].compareArrays) { //likely nested array
            if (!this[i].compareArrays(arr[i])) return false;
            else continue;
        }
        if (this[i] != arr[i]) return false;
    }
    return true;
};


Array.prototype.hasObject = (
  !Array.indexOf ? function (o)
  {
    var l = this.length + 1;
    while (l -= 1)
    {
        if (this[l - 1] === o)
        {
            return true;
        }
    }
    return false;
  } : function (o)
  {
    return (this.indexOf(o) !== -1);
  }
);

Ext.apply(String.prototype, { 
  truncate_length: 25,  
      
  truncate_string: "...",  
      
  truncate: function(truncate_length, truncate_string) {  
    truncate_length = truncate_length || this.truncate_length;  
    truncate_string = truncate_string || this.truncate_string;  
    var length = truncate_length - truncate_string.length;  
    return this.length > truncate_length ? this.substr(0, length) + truncate_string : String(this);  
  } 
});

Ext.override(Ext.data.Record, {
  set: function(name, value) {
    if (String(this.data[name]) == String(value)) return;
    
    this.dirty = true;
    
    if (!this.modified) this.modified = {};
    
    if (typeof this.modified[name] == 'undefined') this.modified[name] = this.data[name];
    
    this.data[name] = value;
    
    if (!this.editing && this.store) this.store.afterEdit(this, name);
  }
});

Ext.override(Ext.data.Store, {
  // private
  afterEdit: function(record, name) {
    if (this.modified.indexOf(record) == -1) {
      this.modified.push(record);
    };
    
    this.fireEvent("update", this, record, Ext.data.Record.EDIT, name);
  }
});

Ext.override(Ext.form.BasicForm, {
  /**
   * Returns the fields in this form as an object with key/value pairs as they would be submitted using a standard form submit.
   * If multiple fields exist with the same name they are returned as an array.
   * @param {Boolean} asString (optional) false to return the values as an object (defaults to returning as a string)
   * @return {String/Object}
  */
  getValues : function(asString){
    var fs = Ext.lib.Ajax.serializeForm(this.el.dom);
    if(asString === true){
      return fs;
    }
    return Ext.urlDecode(fs.replace(/\+/g, ' ')); // /\+/g     forum disabled the backslash
  },
	
	/**
	 * 
	 */
	reset: function() {
		this.items.each(function(f) {
      f.reset();
	  });
    
		this.fireEvent('reset', this);
		
		return this;
	}
	// ,
  // 
  //  	isValid : function(showError){
  //       var valid = true;
  //       this.items.each(function(f){
  //          if(!f.isValid(showError)){
  //              valid = false;
  //          }
  //       });
  //       return valid;
  //   }
});

// // /**
// //  * We have to override this for one messely thing: hover class
// //  * We need a hove class on the this.view dataview and it seems the only way of doing that is
// //  * overriding the whole darn thing, for 1 line (58)
// //  */
// Ext.override(Ext.form.ComboBox, {
//  initComponent: function() {
//    Ext.form.ComboBox.superclass.initComponent.call(this);
//    
//     this.addEvents(
//         'expand',
//         'collapse',
//         'beforeselect',
//         'select',
//         'beforequery'
//     );
//    
//     if(this.transform) {
//       this.allowDomMove = false;
//       var s = Ext.getDom(this.transform);
//    
//       if (!this.hiddenName) this.hiddenName = s.name;
//    
//       if (!this.store) {
//         this.mode = 'local';
//         var d = [], opts = s.options;
//         for(var i = 0, len = opts.length;i < len; i++){
//           var o     = opts[i],
//               value = (o.hasAttribute ? o.hasAttribute('value') : o.getAttributeNode('value').specified) ? o.value : o.text;
//        
//           if (o.selected) this.value = value;
//        
//           d.push([value, o.text]);
//         };
//      
//         this.store = new Ext.data.SimpleStore({
//          'id':   0,
//          fields: ['value', 'text'],
//          data:   d
//         });
//      
//         this.valueField = 'value';
//         this.displayField = 'text';
//       };
//        
//       s.name = Ext.id();
//      
//       if(!this.lazyRender){
//        this.target = true;
//        this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate);
//        Ext.removeNode(s); 
//        this.render(this.el.parentNode);
//       } else {
//         Ext.removeNode(s); 
//       };
//     } else if (Ext.isArray(this.store)) {
//       if (Ext.isArray(this.store[0])) {
//         this.store = new Ext.data.SimpleStore({
//           fields: ['value','text'],
//           data:   this.store
//         });
//        
//         this.valueField = 'value';
//       } else {
//        this.store = new Ext.data.SimpleStore({
//          fields: ['text'],
//          data: this.store,
//          expandData: true
//        });
//        
//        this.valueField = 'text';
//       };
//      
//       this.displayField = 'text';
//       this.mode = 'local';
//     };
//    
//     this.selectedIndex = -1;
//    
//     if (this.mode == 'local') {
//       if (this.initialConfig.queryDelay === undefined) this.queryDelay = 10;
//       if (this.initialConfig.minChars === undefined) this.minChars = 0;
//     };
//    
//    // this.on('focus', function(combo) {
//    //  this.initList();
//    //  if (combo && combo.expand) combo.expand.defer(150, combo);
//    // }, this);
//    //
//    this.on('render', function() {
//      var tempTask = new Ext.util.DelayedTask(function() {
//        var el = this.el.parent();
//        if (el) el.setWidth((this.initialConfig.width) ? this.initialConfig.width - 2 : 'auto');
//      }, this);
//      tempTask.delay(500);
//      
//      if (this.initialConfig.dontAddClickListener === true) return;
//      this.el.on('click', function() {
//        if (this.expand) this.expand.defer(50, this);
//      }, this);
//    }, this);
//  },
//  
// //     // initList: function() {
// //     //   if (!this.list) {
// //     //     var cls = 'x-combo-list';
// //     //  
// //     //     this.list = new Ext.Layer({
// //     //       shadow: this.shadow,
// //     //       cls: [cls, this.listClass].join(' '),
// //     //       constrain: false
// //     //     });
// //     //  
// //     //     var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth);
// //     //     this.list.setWidth(lw);
// //     //     this.list.swallowEvent('mousewheel');
// //     //     this.assetHeight = 0;
// //     //  
// //     //     if (this.title) {
// //     //       this.header = this.list.createChild({
// //     //         cls: cls + '-hd',
// //     //         html: this.title
// //     //       });
// //     //       this.assetHeight += this.header.getHeight();
// //     //     };
// //     //  
// //     //     this.innerList = this.list.createChild({
// //     //       cls: cls + '-inner'
// //     //     });
// //     //      
// //     //     this.innerList.on('mouseover', this.onViewOver, this);
// //     //     this.innerList.on('mousemove', this.onViewMove, this);
// //     //     this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));
// //     //  
// //     //     if (this.pageSize) {
// //     //       this.footer = this.list.createChild({
// //     //         cls: cls + '-ft'
// //     //       });
// //     //        
// //     //       this.pageTb = new Ext.PagingToolbar({
// //     //         store: this.store,
// //     //         pageSize: this.pageSize,
// //     //         renderTo: this.footer
// //     //       });
// //     //        
// //     //       this.assetHeight += this.footer.getHeight();
// //     //     };
// //     //  
// //     //     if (!this.tpl) {
// //     //      this.tpl = '<tpl for="."><div class="' + cls + '-item">{' + this.displayField + '}</div></tpl>';
// //     //     };
// //     //      
// //     //     this.view = new Ext.DataView({
// //     //       applyTo: this.innerList,
// //     //        overClass: 'x-over',
// //     //       tpl: this.tpl,
// //     //       singleSelect: true,
// //     //       selectedClass: this.selectedClass,
// //     //       itemSelector: this.itemSelector || '.' + cls + '-item'
// //     //     });
// //     //  
// //     //     this.view.on('click', this.onViewClick, this);
// //     //  
// //     //     this.bindStore(this.store, true);
// //     //  
// //     //     if (this.resizable) {
// //     //       this.resizer = new Ext.Resizable(this.list, {
// //     //         pinned: true,
// //     //         handles: 'se'
// //     //       });
// //     //        
// //     //       this.resizer.on('resize', function(r, w, h) {
// //     //         this.maxHeight = h - this.handleHeight - this.list.getFrameWidth('tb') - this.assetHeight;
// //     //         this.listWidth = w;
// //     //         this.innerList.setWidth(w - this.list.getFrameWidth('lr'));
// //     //         this.restrictHeight();
// //     //       }, this);
// //     //  
// //     //       this[this.pageSize ? 'footer': 'innerList'].setStyle('margin-bottom', this.handleHeight + 'px');
// //     //     };
// //     //   }
// //     // },
// //   
//  /**
//   * Here we need to override the onViewClick method to fire a click event. Useful for
//   * getting the targetEl on click...
//   */
//  onViewClick: function(doFocus, index, node, e) {
//    this.fireEvent('click', doFocus, index, node, e);
//    
//    var index = this.view.getSelectedIndexes()[0];
//    var r = this.store.getAt(index);
//    
//    if (r) this.onSelect(r, index);
//    
//    if (doFocus !== false) this.el.focus();
//  }
// });

// Ext.override(Ext.form.DateField, {
//  initComponent : function(){
//    Ext.form.DateField.superclass.initComponent.call(this);
// 
//    this.addEvents(
//      'select'
//    );
// 
//    if(typeof this.minValue == "string") this.minValue = this.parseDate(this.minValue);
//    if(typeof this.maxValue == "string") this.maxValue = this.parseDate(this.maxValue);
//    
//    this.disabledDatesRE = null;
//    this.initDisabledDays();
//    
//     // this.on('render', this.initTriggerClicker, this);
//  }
//  
//   // initTriggerClicker: function() {
//   //  if (this.initialConfig.dontAddClickListener === true) return;
//   //  this.el.on('click', function() {
//   //    this.onTriggerClick();
//   //  }, this);
//   //  
//   //     // if (!this.focused) {
//   //     //      this.el.on('focus', function() {
//   //     //        this.onTriggerClick();
//   //     //        this.focused = true;
//   //     //      }, this);
//   //     //    };
//   // }
// });

Ext.override(Ext.form.Field, {
  afterRender: function(){
    Ext.form.Field.superclass.afterRender.call(this);
    this.initEvents();
    this.initValue();
    
    this.el.on("keyup", function() {
      this.fireEvent('change', this);
    }, this);
  },
  
  onBlur : function(){
    this.beforeBlur();
    if(!Ext.isOpera && this.focusClass){ // don't touch in Opera
      this.el.removeClass(this.focusClass);
    }
    this.hasFocus = false;
    if(this.validationEvent !== false && this.validateOnBlur && this.validationEvent != "blur"){
      this.validate();
    }
    var v = this.getValue();
    if(String(v) !== String(this.startValue)){
      this.fireEvent('change', this, v, this.startValue);
    }
    this.fireEvent("blur", this);
  },

  getFormPanel: function() {
    if (!this.formPanel) {
      this.formPanel = this.findParentByType('form');
    }
    return this.formPanel;
  }
});

Ext.override(Ext.form.TextArea, {
  growPad: -14,
  
  autoSize: function(){
    if (!this.grow || !this.textSizeEl) return;
    
    var el = this.el,
        v  = el.dom.value,
        ts = this.textSizeEl;
    
    ts.innerHTML = '';
    ts.appendChild(document.createTextNode(v));
    
    v = ts.innerHTML;
    Ext.fly(ts).setWidth(this.el.getWidth());
    
    if (v.length < 1) {
      v = "&#160;&#160;";
    } else {
      v += this.growAppend;
      if (Ext.isIE) v = v.replace(/\n/g, '<br />');
    };
    
    ts.innerHTML = v;
    
    var extra = ts.offsetHeight / 100 * 4;
    
    var h = Math.min(this.growMax, Math.max(ts.offsetHeight, this.growMin) + this.growPad) - extra;
    if (h != this.lastHeight) {
      this.lastHeight = h;
      this.el.setHeight(h);
      this.fireEvent("autosize", this, h);
    }
  }
});

Ext.override(Ext.form.TriggerField,  {
  onResize: function(w, h) {
    Ext.form.TriggerField.superclass.onResize.call(this, w, h);
    
    if (typeof w == 'number') { this.el.setWidth(this.adjustWidth('input', w)); };
    
    this.wrap.setWidth(this.el.getWidth());
  },
  
  onRender: function(ct, position) {
    Ext.form.TriggerField.superclass.onRender.call(this, ct, position);
    
    // Wrap the whole trigger field in a div
    this.wrap = this.el.wrap({cls: "x-form-field-wrap x-trigger-field-wrap"});
    
    // Create the trigger
    this.trigger = this.wrap.createChild({cls: "x-form-trigger " + this.triggerClass});
    
    // Hide the trigger on load if render
    if (this.hideTrigger) { this.trigger.setDisplayed(false); };
    
    this.initTrigger();
    
    if (!this.width) { this.wrap.setWidth(this.el.getWidth()); };
  }
});

Ext.apply(Ext.form.VTypes, {
  urlWithoutHttp: function(v) {
    var url = /(([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i;
    return url.test(v);
  },
  
  password: function(value, field) {
    var valid = false;
    
    if (field.matches) {
      var otherField = Ext.getCmp(field.matches);
			
      if (value == otherField.getValue()) {
        otherField.clearInvalid();
        valid = true;
      };
    };
    
    return valid;
  },
  passwordText: 'Passwords do not match.',
	
	creditcard: function(value, field) {
		return value.replace(/[ \-]/g,'').length == 16;
	},
	creditcardText: 'Invalid credit card number',
	creditcardMask: /[ \d\-]/,
	
	bdayyear: function(value, field) {
	  return value > (new Date().format('Y') - 14);
	}
});

/**
 * Overrides Ext.Shadow.prototype for custom shadows on windows
 */
Ext.apply(Ext.Shadow.prototype, {
  
  realign : function(l, t, w, h){
    if(!this.el){
      return;
    }
    var a = this.adjusts, d = this.el.dom, s = d.style;
    var iea = 0;
    s.left = (l+a.l)+"px";
    s.top = (t+a.t)+"px";
    var sw = (w+a.w), sh = (h+a.h), sws = sw +"px", shs = sh + "px";
    
    if(s.width != sws || s.height != shs){
      s.width = sws;
      s.height = shs;
      if(!Ext.isIE){
        var cn = d.childNodes;
        var sww = Math.max(0, (sw-36))+"px";
        cn[0].childNodes[1].style.width = sww;
        cn[1].childNodes[1].style.width = sww;
        cn[2].childNodes[1].style.width = sww;
        cn[1].style.height = Math.max(0, (sh-36))+"px";
      }
    }
  }
  
});

/**
 * @class ExtMVC.plugin.LeftMenuViewportBuilder
 * @extends ExtMVC.ViewportBuilder
 * Creates a viewport with a left menu and a main tabpanel.
 */
ExtMVC.plugin.LeftMenuViewportBuilder = function() {
  ExtMVC.plugin.LeftMenuViewportBuilder.superclass.constructor.call(this);
};
Ext.extend(ExtMVC.plugin.LeftMenuViewportBuilder, ExtMVC.ViewportBuilder, {
  
  /**
   * Creates the viewport elements, taking config options from OS.viewportBuilderConfig
   * @param {ExtMVC.OS} os The OS instance to link built viewport components to
   * Assigns this.topBar, this.sideMenu, this.mainPanel, this.contentPanel and this.viewport
   * to the passed os when calling build(os)
   * @return {ExtMVC.OS} The same OS instance, now decorated with topBar, sideMenu, mainPanel,
   * contentPanel and viewport
   */
  build: function(os) {
    var config  = os.getViewportBuilderConfig() || {};
    config.menu = config.menu || {};
    
    //menu items are placed into a child container, allow config.menu.items to
    //pass straight through to the child
    config.menu.items = [
      new Ext.Panel({
        bodyStyle: 'background-color: #DFE8F6;',
        items:     config.menu.items,
        defaults:  {
          xtype: 'menu_link'
        }
      })
    ];
    
    var viewportItems = [];
    
    if (config.topBar) {
      //config.topBar could either be true, or an object.  Normalise into an object
      config.topBar = (typeof(config.topBar) == 'object' ? config.topBar : {});
      
      os.topBar = Ext.applyIf(config.topBar, {
        region: 'north',
        cls:    'x-mvc-topbar'
      });
    
      viewportItems.push(os.topBar);
    };
    
    os.sideMenu = new Ext.Panel(Ext.applyIf(config.menu, {
      region:      'west',
      width:       200,
      frame:       true,
      margins:     '8 0 8 8',
      title:       'Menu',
      split:       true,
      collapsible: true,
      autoScroll:  true,
      bodyStyle:   'background-color: #DFE8F6;'
    }));
    
    os.mainPanel = this.createMainPanel(config);
    
    os.contentPanel = new Ext.Panel({
      region:    'center',
      layout:    'border',
      bodyStyle: 'background-color: #c5d1e7; padding: 10px;',
      items:     [os.sideMenu, os.mainPanel]
    });
    
    viewportItems.push(os.contentPanel);
    
    os.viewport = new Ext.Viewport({
      layout:    'border',
      bodyStyle: 'background-color: #c5d1e7; margin-bottom: 10px;',
      items:     viewportItems
    });
            
    //Tell controllers to add views to the mainPanel instead of rendering directly
    ExtMVC.Controller.prototype.addTo = os.mainPanel;
    
    //don't render views automatically, return a renderable instantiation instead (a subclass of Ext.Component)
    ExtMVC.Controller.prototype.renderMethod = 'add';
    
    return os;
  },
  
  //private - decides whether to create a TabPanel or not, based on os.viewportBuilderConfig.useTabs
  createMainPanel: function(config) {
    var config  = config || {};
    config.main = config.main || {};
    Ext.applyIf(config.main, {
      region:   'center',
      border:   false,
      defaults: { frame: true }
    });

    if (config.useTabs) {
      return new Ext.TabPanel(Ext.applyIf(config.main, {
        margins:    '8 8 8 0',
        items:      []
      }));
    } else {
      return new Ext.Panel(Ext.applyIf(config.main, {
        margins:    '0 8 8 0',
        layout:     'fit',
        bodyBorder: false,
        bodyStyle:  'padding-top: 8px; background-color: #c5d1e7;',
        listeners:  {
          'beforeadd': {
            //removes the current component so newly added component shows up automatically
            fn: function(ct, component, index) {
              if (ct.items) {
                ct.items.each(function(i) {this.remove(i);}, this);
              };
            }
          }
        },
        items: [{
          hideMode:  'offsets',
          bodyStyle: 'background-color: #c5d1e7;',
          iconCls:   'home',
          border:    false
        }]
      }));
    };
  }
  
});

ExtMVC.ViewportBuilderManager.register('leftmenu', ExtMVC.plugin.LeftMenuViewportBuilder);

/**
 * FIXME: ugh, don't code tired, you write shit like this:
 * 
 * @class ExtMVC.plugin.LeftMenuViewportBuilder.MenuLink
 * @extends Ext.Component
 * Renders a basic link which fires an event when clicked
 */
ExtMVC.plugin.LeftMenuViewportBuilder.MenuLink = function(config) {
  var config = config || {};
 
  Ext.applyIf(config, {
    html: '<a href="#' + config.url + '" cls="' + config.cls + '">' + config.text + '</a>'
  });
  
  config.cls += ' x-menu-link';
  delete config.xtype ;
  delete config.url;
  delete config.text;
 
  ExtMVC.plugin.LeftMenuViewportBuilder.MenuLink.superclass.constructor.call(this, config);
};
Ext.extend(ExtMVC.plugin.LeftMenuViewportBuilder.MenuLink, Ext.Component, {
  
  onRender: function(ct, position) {
    this.el = ct.createChild(this.initialConfig);
    
    this.el.on('click', this.initialConfig.handler, this);
    
    ExtMVC.plugin.LeftMenuViewportBuilder.MenuLink.superclass.onRender.apply(this, arguments);
  }
});

Ext.reg('menu_link', ExtMVC.plugin.LeftMenuViewportBuilder.MenuLink);

/**
 * @class ExtMVC.plugin.LeftMenuViewportBuilder.MenuHeader
 * @extends Ext.Component
 * Renders a header divider element in a menu
 */
ExtMVC.plugin.LeftMenuViewportBuilder.MenuHeader = function(config) {
  var config = config || {};
 
  Ext.applyIf(config, {
    title: 'Header'
  });
 
  ExtMVC.plugin.LeftMenuViewportBuilder.MenuHeader.superclass.constructor.call(this, config);
};
Ext.extend(ExtMVC.plugin.LeftMenuViewportBuilder.MenuHeader, Ext.Component, {
  /**
   * Renders the HTML for this component
   * @param {Ext.Container} ct The container to render this component to
   * @param {Number} position The position within the parent container to render this component to
   */
  onRender: function(ct, position) {
    this.el = ct.createChild({
      tag: 'h1',
      cls: 'x-menu-header',
      html: this.initialConfig.title
    });
  
    ExtMVC.plugin.LeftMenuViewportBuilder.MenuHeader.superclass.onRender.apply(this, arguments);
  }
});

Ext.reg('menu_header', ExtMVC.plugin.LeftMenuViewportBuilder.MenuHeader);

(function(){var EV=Ext.lib.Event;Ext.ux.ManagedIFrame=function(){var args=Array.prototype.slice.call(arguments,0),el=Ext.get(args[0]),config=args[0];if(el&&el.dom&&el.dom.tagName=="IFRAME"){config=args[1]||{};}else{config=args[0]||args[1]||{};el=config.autoCreate?Ext.get(Ext.DomHelper.append(config.autoCreate.parent||document.body,Ext.apply({tag:"iframe",src:(Ext.isIE&&Ext.isSecure)?Ext.SSL_SECURE_URL:""},config.autoCreate))):null;}if(!el||el.dom.tagName!="IFRAME"){return el;}el.dom.name||(el.dom.name=el.dom.id);this.addEvents({"focus":true,"blur":true,"unload":true,"domready":true,"documentloaded":true,"exception":true,"message":true});if(config.listeners){this.listeners=config.listeners;Ext.ux.ManagedIFrame.superclass.constructor.call(this);}Ext.apply(el,this);el.addClass("x-managed-iframe");if(config.style){el.applyStyles(config.style);}el._maskEl=el.parent(".x-managed-iframe-mask")||el.parent().addClass("x-managed-iframe-mask");Ext.apply(el,{disableMessaging:config.disableMessaging===true,loadMask:Ext.apply({msg:"Loading..",msgCls:"x-mask-loading",maskEl:el._maskEl,hideOnReady:true,disabled:!config.loadMask},config.loadMask),_eventName:Ext.isIE?"onreadystatechange":"onload",_windowContext:null,eventsFollowFrameLinks:typeof config.eventsFollowFrameLinks=="undefined"?true:config.eventsFollowFrameLinks});el.dom[el._eventName]=el.loadHandler.createDelegate(el);if(document.addEventListener){Ext.EventManager.on(window,"DOMFrameContentLoaded",el.dom[el._eventName]);}var um=el.updateManager=new Ext.UpdateManager(el,true);um.showLoadIndicator=config.showLoadIndicator||false;if(config.src){el.setSrc(config.src);}else{var content=config.html||config.content||false;if(content){el.update.defer(10,el,[content]);}}return Ext.ux.ManagedIFrame.Manager.register(el);};var MIM=Ext.ux.ManagedIFrame.Manager=function(){var frames={};return{shimCls:"x-frame-shim",register:function(frame){frame.manager=this;frames[frame.id]=frames[frame.dom.name]={ref:frame,elCache:{}};return frame;},deRegister:function(frame){frame._unHook();delete frames[frame.id];delete frames[frame.dom.name];},hideShims:function(){if(!this.shimApplied){return ;}Ext.select("."+this.shimCls,true).removeClass(this.shimCls+"-on");this.shimApplied=false;},showShims:function(){if(!this.shimApplied){this.shimApplied=true;Ext.select("."+this.shimCls,true).addClass(this.shimCls+"-on");}},getFrameById:function(id){return typeof id=="string"?(frames[id]?frames[id].ref||null:null):null;},getFrameByName:function(name){return this.getFrameById(name);},getFrameHash:function(frame){return frame.id?frames[frame.id]:null;},eventProxy:function(e){e=Ext.lib.Event.getEvent(e);if(!e){return ;}var be=e.browserEvent||e;if(e.type=="unload"){this._unHook();}if(!be["eventPhase"]||(be["eventPhase"]==(be["AT_TARGET"]||2))){return this.fireEvent(e.type,e);}},_flyweights:{},removeNode:Ext.isIE?function(frame,n){frame=MIM.getFrameHash(frame);if(frame&&n&&n.tagName!="BODY"){d=frame.scratchDiv||(frame.scratchDiv=frame.getDocument().createElement("div"));d.appendChild(n);d.innerHTML="";}}:function(frame,n){if(n&&n.parentNode&&n.tagName!="BODY"){n.parentNode.removeChild(n);}}};}();MIM.showDragMask=MIM.showShims;MIM.hideDragMask=MIM.hideShims;MIM.El=function(frame,el,forceNew){var frameObj;frame=(frameObj=MIM.getFrameHash(frame))?frameObj.ref:null;if(!frame){return null;}var elCache=frameObj.elCache||(frameObj.elCache={});var dom=frame.getDom(el);if(!dom){return null;}var id=dom.id;if(forceNew!==true&&id&&elCache[id]){return elCache[id];}this.dom=dom;this.id=id||Ext.id(dom);};MIM.El.get=function(frame,el){var ex,elm,id,doc;if(!frame||!el){return null;}var frameObj;frame=(frameObj=MIM.getFrameHash(frame))?frameObj.ref:null;if(!frame){return null;}var elCache=frameObj.elCache||(frameObj.elCache={});if(!(doc=frame.getDocument())){return null;}if(typeof el=="string"){if(!(elm=frame.getDom(el))){return null;}if(ex=elCache[el]){ex.dom=elm;}else{ex=elCache[el]=new MIM.El(frame,elm);}return ex;}else{if(el.tagName){if(!(id=el.id)){id=Ext.id(el);}if(ex=elCache[id]){ex.dom=el;}else{ex=elCache[id]=new MIM.El(frame,el);}return ex;}else{if(el instanceof MIM.El){if(el!=frameObj.docEl){el.dom=frame.getDom(el.id)||el.dom;elCache[el.id]=el;}return el;}else{if(el.isComposite){return el;}else{if(Ext.isArray(el)){return frame.select(el);}else{if(el==doc){if(!frameObj.docEl){var f=function(){};f.prototype=MIM.El.prototype;frameObj.docEl=new f();frameObj.docEl.dom=doc;}return frameObj.docEl;}}}}}}return null;};Ext.apply(MIM.El.prototype,Ext.Element.prototype);Ext.extend(Ext.ux.ManagedIFrame,Ext.util.Observable,{src:null,setSrc:function(url,discardUrl,callback){var reset=Ext.isIE&&Ext.isSecure?Ext.SSL_SECURE_URL:"";var src=url||this.src||reset;if(Ext.isOpera){this.dom.src=reset;}this._windowContext=null;this._unHook();this._callBack=callback||false;this.showMask();(function(){var s=typeof src=="function"?src()||"":src;try{this._frameAction=true;this.dom.src=s;this.frameInit=true;this.checkDOM();}catch(ex){this.fireEvent("exception",this,ex);}}).defer(10,this);if(discardUrl!==true){this.src=src;}return this;},reset:function(src,callback){this.setSrc(src||(Ext.isIE&&Ext.isSecure?Ext.SSL_SECURE_URL:""),true,callback);},scriptRE:/(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/gi,update:function(content,loadScripts,callback){loadScripts=loadScripts||this.getUpdateManager().loadScripts||false;content=Ext.DomHelper.markup(content||"");content=loadScripts===true?content:content.replace(this.scriptRE,"");var doc;if(doc=this.getDocument()){this._frameAction=!!content.length;this._windowContext=this.src=null;this._callBack=callback||false;this._unHook();this.showMask();doc.open();doc.write(content);doc.close();this.frameInit=true;if(this._frameAction){this.checkDOM();}else{this.hideMask(true);if(this._callBack){this._callBack();}}}else{this.hideMask(true);if(this._callBack){this._callBack();}}return this;},disableMessaging:true,_XFrameMessaging:function(){var tagStack={"$":[]};var isEmpty=function(v,allowBlank){return v===null||v===undefined||(!allowBlank?v==="":false);};window.sendMessage=function(message,tag,origin){var MIF;if(MIF=arguments.callee.manager){if(message._fromHost){var fn,result;var compTag=message.tag||tag||null;var mstack=!isEmpty(compTag)?tagStack[compTag.toLowerCase()]||[]:tagStack["$"];for(var i=0,l=mstack.length;i<l;i++){if(fn=mstack[i]){result=fn.apply(fn.__scope,arguments)===false?false:result;if(fn.__single){mstack[i]=null;}if(result===false){break;}}}return result;}else{message={type:isEmpty(tag)?"message":"message:"+tag.toLowerCase().replace(/^\s+|\s+$/g,""),data:message,domain:origin||document.domain,uri:document.documentURI,source:window,tag:isEmpty(tag)?null:tag.toLowerCase()};try{return MIF.disableMessaging!==true?MIF.fireEvent.call(MIF,message.type,MIF,message):null;}catch(ex){}return null;}}};window.onhostmessage=function(fn,scope,single,tag){if(typeof fn=="function"){if(!isEmpty(fn.__index)){throw"onhostmessage: duplicate handler definition"+(tag?" for tag:"+tag:"");}var k=isEmpty(tag)?"$":tag.toLowerCase();tagStack[k]||(tagStack[k]=[]);Ext.apply(fn,{__tag:k,__single:single||false,__scope:scope||window,__index:tagStack[k].length});tagStack[k].push(fn);}else{throw"onhostmessage: function required";}};window.unhostmessage=function(fn){if(typeof fn=="function"&&typeof fn.__index!="undefined"){var k=fn.__tag||"$";tagStack[k][fn.__index]=null;}};},get:function(el){return MIM.El.get(this,el);},fly:function(el,named){named=named||"_global";el=this.getDom(el);if(!el){return null;}if(!MIM._flyweights[named]){MIM._flyweights[named]=new Ext.Element.Flyweight();}MIM._flyweights[named].dom=el;return MIM._flyweights[named];},getDom:function(el){var d;if(!el||!(d=this.getDocument())){return null;}return el.dom?el.dom:(typeof el=="string"?d.getElementById(el):el);},select:function(selector,unique){var d;return(d=this.getDocument())?Ext.Element.select(selector,unique,d):null;},query:function(selector){var d;return(d=this.getDocument())?Ext.DomQuery.select(selector,d):null;},getDoc:function(){return this.get(this.getDocument());},removeNode:function(node){MIM.removeNode(this,this.getDom(node));},_unHook:function(){var elcache,h=MIM.getFrameHash(this)||{};if(this._hooked&&h&&(elcache=h.elCache)){for(var id in elcache){var el=elcache[id];delete elcache[id];if(el.removeAllListeners){el.removeAllListeners();}}if(h.docEl){h.docEl.removeAllListeners();h.docEl=null;delete h.docEl;}}this._hooked=this._domReady=this._domFired=false;},_renderHook:function(){this._windowContext=null;this._hooked=false;try{if(this.writeScript('(function(){(window.hostMIF = parent.Ext.get("'+this.dom.id+'"))._windowContext='+(Ext.isIE?"window":"{eval:function(s){return eval(s);}}")+";})();")){this._frameProxy||(this._frameProxy=MIM.eventProxy.createDelegate(this));EV.doAdd(this.getWindow(),"focus",this._frameProxy);EV.doAdd(this.getWindow(),"blur",this._frameProxy);EV.doAdd(this.getWindow(),"unload",this._frameProxy);if(this.disableMessaging!==true){this.loadFunction({name:"XMessage",fn:this._XFrameMessaging},false,true);var sm;if(sm=this.getWindow().sendMessage){sm.manager=this;}}this.CSS=new CSSInterface(this.getDocument());}return this.domWritable();}catch(ex){}return false;},sendMessage:function(message,tag,origin){var win;if(this.disableMessaging!==true&&(win=this.getWindow())){tag||(tag=message.tag||"");tag=tag.toLowerCase();message=Ext.applyIf(message.data?message:{data:message},{type:Ext.isEmpty(tag)?"message":"message:"+tag,domain:origin||document.domain,uri:document.documentURI,source:window,tag:tag||null,_fromHost:this});return win.sendMessage?win.sendMessage.call(null,message,tag,origin):null;}return null;},_windowContext:null,getDocument:function(){var win;return(win=this.getWindow())?win.document:null;},getBody:function(){var d;return(d=this.getDocument())?d.body:null;},getDocumentURI:function(){var URI;try{URI=this.src?this.getDocument().location.href:null;}catch(ex){}return URI||this.src;},getWindow:function(){var dom=this.dom;return dom?dom.contentWindow||window.frames[dom.name]:null;},print:function(){try{var win=this.getWindow();if(Ext.isIE){win.focus();}win.print();}catch(ex){throw"print exception: "+(ex.description||ex.message||ex);}},destroy:function(){this.removeAllListeners();if(this.dom){if(document.addEventListener){Ext.EventManager.un(window,"DOMFrameContentLoaded",this.dom[this._eventName]);}this.dom[this._eventName]=null;Ext.ux.ManagedIFrame.Manager.deRegister(this);this._windowContext=null;if(Ext.isIE&&this.dom.src){this.dom.src="javascript:false";}this._maskEl=null;Ext.removeNode(this.dom);}Ext.apply(this.loadMask,{masker:null,maskEl:null});},domWritable:function(){return !!this._windowContext;},execScript:function(block,useDOM){try{if(this.domWritable()){if(useDOM){this.writeScript(block);}else{return this._windowContext.eval(block);}}else{throw"execScript:non-secure context";}}catch(ex){this.fireEvent("exception",this,ex);return false;}return true;},writeScript:function(block,attributes){attributes=Ext.apply({},attributes||{},{type:"text/javascript",text:block});try{var head,script,doc=this.getDocument();if(doc&&doc.getElementsByTagName){if(!(head=doc.getElementsByTagName("head")[0])){head=doc.createElement("head");doc.getElementsByTagName("html")[0].appendChild(head);}if(head&&(script=doc.createElement("script"))){for(var attrib in attributes){if(attributes.hasOwnProperty(attrib)&&attrib in script){script[attrib]=attributes[attrib];}}return !!head.appendChild(script);}}}catch(ex){this.fireEvent("exception",this,ex);}return false;},loadFunction:function(fn,useDOM,invokeIt){var name=fn.name||fn;var fn=fn.fn||window[fn];this.execScript(name+"="+fn,useDOM);if(invokeIt){this.execScript(name+"()");}},showMask:function(msg,msgCls,forced){var lmask;if((lmask=this.loadMask)&&(!lmask.disabled||forced)){if(lmask._vis){return ;}lmask.masker||(lmask.masker=Ext.get(lmask.maskEl||this.dom.parentNode||this.wrap({tag:"div",style:{position:"relative"}})));lmask._vis=true;lmask.masker.mask.defer(lmask.delay||5,lmask.masker,[msg||lmask.msg,msgCls||lmask.msgCls]);}},hideMask:function(forced){var tlm;if((tlm=this.loadMask)&&!tlm.disabled&&tlm.masker){if(!forced&&(tlm.hideOnReady!==true&&this._domReady)){return ;}tlm._vis=false;tlm.masker.unmask.defer(tlm.delay||5,tlm.masker);}},loadHandler:function(e){if(!this.frameInit||(!this._frameAction&&!this.eventsFollowFrameLinks)){return ;}var rstatus=(e&&typeof e.type!=="undefined"?e.type:this.dom.readyState);switch(rstatus){case"loading":case"interactive":break;case"DOMFrameContentLoaded":if(this._domFired||(e&&e.target!==this.dom)){return ;}case"domready":if(this._domFired){return ;}if(this._domFired=this._hooked=this._renderHook()){this._frameAction=(this.fireEvent("domready",this)===false?false:this._frameAction);}case"domfail":this._domReady=true;this.hideMask();break;case"load":case"complete":if(!this._domFired){this.loadHandler({type:"domready"});}this.hideMask(true);if(this._frameAction||this.eventsFollowFrameLinks){this.fireEvent.defer(50,this,["documentloaded",this]);}this._frameAction=false;if(this.eventsFollowFrameLinks){this._domFired=this._domReady=false;}if(this._callBack){this._callBack(this);}break;default:}},checkDOM:function(win){if(Ext.isOpera){return ;}var n=0,win=win||this.getWindow(),manager=this,domReady=false,max=100;var poll=function(){try{domReady=false;var doc=win.document,body;if(!manager._domReady){domReady=(doc&&doc.getElementsByTagName);domReady=domReady&&(body=doc.getElementsByTagName("body")[0])&&!!body.innerHTML.length;}}catch(ex){n=max;}if(!manager._frameAction||manager._domReady){return ;}if(n++<max&&!domReady){setTimeout(arguments.callee,10);return ;}manager.loadHandler({type:domReady?"domready":"domfail"});};setTimeout(poll,50);}});var styleCamelRe=/(-[a-z])/gi;var styleCamelFn=function(m,a){return a.charAt(1).toUpperCase();};var CSSInterface=function(hostDocument){var doc;if(hostDocument){doc=hostDocument;return{rules:null,createStyleSheet:function(cssText,id){var ss;if(!doc){return ;}var head=doc.getElementsByTagName("head")[0];var rules=doc.createElement("style");rules.setAttribute("type","text/css");if(id){rules.setAttribute("id",id);}if(Ext.isIE){head.appendChild(rules);ss=rules.styleSheet;ss.cssText=cssText;}else{try{rules.appendChild(doc.createTextNode(cssText));}catch(e){rules.cssText=cssText;}head.appendChild(rules);ss=rules.styleSheet?rules.styleSheet:(rules.sheet||doc.styleSheets[doc.styleSheets.length-1]);}this.cacheStyleSheet(ss);return ss;},removeStyleSheet:function(id){if(!doc){return ;}var existing=doc.getElementById(id);if(existing){existing.parentNode.removeChild(existing);}},swapStyleSheet:function(id,url){this.removeStyleSheet(id);if(!doc){return ;}var ss=doc.createElement("link");ss.setAttribute("rel","stylesheet");ss.setAttribute("type","text/css");ss.setAttribute("id",id);ss.setAttribute("href",url);doc.getElementsByTagName("head")[0].appendChild(ss);},refreshCache:function(){return this.getRules(true);},cacheStyleSheet:function(ss){if(this.rules){this.rules={};}try{var ssRules=ss.cssRules||ss.rules;for(var j=ssRules.length-1;j>=0;--j){this.rules[ssRules[j].selectorText]=ssRules[j];}}catch(e){}},getRules:function(refreshCache){if(this.rules==null||refreshCache){this.rules={};if(doc){var ds=doc.styleSheets;for(var i=0,len=ds.length;i<len;i++){try{this.cacheStyleSheet(ds[i]);}catch(e){}}}}return this.rules;},getRule:function(selector,refreshCache){var rs=this.getRules(refreshCache);if(!Ext.isArray(selector)){return rs[selector];}for(var i=0;i<selector.length;i++){if(rs[selector[i]]){return rs[selector[i]];}}return null;},updateRule:function(selector,property,value){if(!Ext.isArray(selector)){var rule=this.getRule(selector);if(rule){rule.style[property.replace(styleCamelRe,styleCamelFn)]=value;return true;}}else{for(var i=0;i<selector.length;i++){if(this.updateRule(selector[i],property,value)){return true;}}}return false;}};}};Ext.ux.ManagedIframePanel=Ext.extend(Ext.Panel,{defaultSrc:null,bodyStyle:{height:"100%",width:"100%",position:"relative"},frameStyle:{overflow:"auto"},frameConfig:null,hideMode:!Ext.isIE?"nosize":"display",shimCls:Ext.ux.ManagedIFrame.Manager.shimCls,shimUrl:null,loadMask:false,animCollapse:Ext.isIE,autoScroll:false,closable:true,ctype:"Ext.ux.ManagedIframePanel",showLoadIndicator:false,unsupportedText:"Inline frames are NOT enabled/supported by your browser.",initComponent:function(){var unsup=this.unsupportedText?{html:this.unsupportedText}:false;this.bodyCfg||(this.bodyCfg={tag:"div",cls:"x-panel-body",children:[{cls:"x-managed-iframe-mask",children:[Ext.apply(Ext.apply({tag:"iframe",frameborder:0,cls:"x-managed-iframe",style:this.frameStyle||null},this.frameConfig),unsup,Ext.isIE&&Ext.isSecure?{src:Ext.SSL_SECURE_URL}:false),{tag:"img",src:this.shimUrl||Ext.BLANK_IMAGE_URL,cls:this.shimCls}]}]});this.autoScroll=false;this.items=null;if(this.stateful!==false){this.stateEvents||(this.stateEvents=["documentloaded"]);}Ext.ux.ManagedIframePanel.superclass.initComponent.call(this);this.monitorResize||(this.monitorResize=this.fitToParent);this.addEvents({documentloaded:true,domready:true,message:true,exception:true});this.addListener=this.on;},doLayout:function(){if(this.fitToParent&&!this.ownerCt){var pos=this.getPosition(),size=(Ext.get(this.fitToParent)||this.getEl().parent()).getViewSize();this.setSize(size.width-pos[0],size.height-pos[1]);}Ext.ux.ManagedIframePanel.superclass.doLayout.apply(this,arguments);},beforeDestroy:function(){if(this.rendered){if(this.tools){for(var k in this.tools){Ext.destroy(this.tools[k]);}}if(this.header&&this.headerAsText){var s;if(s=this.header.child("span")){s.remove();}this.header.update("");}Ext.each(["iframe","shim","header","topToolbar","bottomToolbar","footer","loadMask","body","bwrap"],function(elName){if(this[elName]){if(typeof this[elName].destroy=="function"){this[elName].destroy();}else{Ext.destroy(this[elName]);}this[elName]=null;delete this[elName];}},this);}Ext.ux.ManagedIframePanel.superclass.beforeDestroy.call(this);},onDestroy:function(){Ext.Panel.superclass.onDestroy.call(this);},onRender:function(ct,position){Ext.ux.ManagedIframePanel.superclass.onRender.call(this,ct,position);if(this.iframe=this.body.child("iframe.x-managed-iframe")){var El=Ext.Element;var mode=El[this.hideMode.toUpperCase()]||"x-hide-nosize";Ext.each([this[this.collapseEl],this.floating?null:this.getActionEl(),this.iframe],function(el){if(el){el.setVisibilityMode(mode);}},this);if(this.loadMask){this.loadMask=Ext.apply({disabled:false,maskEl:this.body,hideOnReady:true},this.loadMask);}if(this.iframe=new Ext.ux.ManagedIFrame(this.iframe,{loadMask:this.loadMask,showLoadIndicator:this.showLoadIndicator,disableMessaging:this.disableMessaging,style:this.frameStyle})){this.loadMask=this.iframe.loadMask;this.iframe.ownerCt=this;this.relayEvents(this.iframe,["blur","focus","unload","documentloaded","domready","exception","message"].concat(this._msgTagHandlers||[]));delete this._msgTagHandlers;}this.getUpdater().showLoadIndicator=this.showLoadIndicator||false;var ownerCt=this.ownerCt;while(ownerCt){ownerCt.on("afterlayout",function(container,layout){var MIM=Ext.ux.ManagedIFrame.Manager,st=false;Ext.each(["north","south","east","west"],function(region){var reg;if((reg=layout[region])&&reg.splitEl){st=true;if(!reg.split._splitTrapped){reg.split.on("beforeresize",MIM.showShims,MIM);reg.split._splitTrapped=true;}}},this);if(st&&!this._splitTrapped){this.on("resize",MIM.hideShims,MIM);this._splitTrapped=true;}},this,{single:true});ownerCt=ownerCt.ownerCt;}}this.shim=Ext.get(this.body.child("."+this.shimCls));},toggleShim:function(){if(this.shim&&this.shimCls){this.shim.toggleClass(this.shimCls+"-on");}},afterRender:function(container){var html=this.html;delete this.html;Ext.ux.ManagedIframePanel.superclass.afterRender.call(this);if(this.iframe){if(this.defaultSrc){this.setSrc();}else{if(html){this.iframe.update(typeof html=="object"?Ext.DomHelper.markup(html):html);}}}},sendMessage:function(){if(this.iframe){this.iframe.sendMessage.apply(this.iframe,arguments);}},on:function(name){var tagRE=/^message\:/i,n=null;if(typeof name=="object"){for(var na in name){if(!this.filterOptRe.test(na)&&tagRE.test(na)){n||(n=[]);n.push(na.toLowerCase());}}}else{if(tagRE.test(name)){n=[name.toLowerCase()];}}if(this.getFrame()&&n){this.relayEvents(this.iframe,n);}else{this._msgTagHandlers||(this._msgTagHandlers=[]);if(n){this._msgTagHandlers=this._msgTagHandlers.concat(n);}}Ext.ux.ManagedIframePanel.superclass.on.apply(this,arguments);},setSrc:function(url,discardUrl,callback){url=url||this.defaultSrc||false;if(!url){return this;}if(url.url){callback=url.callback||false;discardUrl=url.discardUrl||false;url=url.url||false;}var src=url||(Ext.isIE&&Ext.isSecure?Ext.SSL_SECURE_URL:"");if(this.rendered&&this.iframe){this.iframe.setSrc(src,discardUrl,callback);}return this;},getState:function(){var URI=this.iframe?this.iframe.getDocumentURI()||null:null;return Ext.apply(Ext.ux.ManagedIframePanel.superclass.getState.call(this)||{},URI?{defaultSrc:typeof URI=="function"?URI():URI}:null);},getUpdater:function(){return this.rendered?(this.iframe||this.body).getUpdater():null;},getFrame:function(){return this.rendered?this.iframe:null;},getFrameWindow:function(){return this.rendered&&this.iframe?this.iframe.getWindow():null;},getFrameDocument:function(){return this.rendered&&this.iframe?this.iframe.getDocument():null;},getFrameDoc:function(){return this.rendered&&this.iframe?this.iframe.getDoc():null;},getFrameBody:function(){return this.rendered&&this.iframe?this.iframe.getBody():null;},load:function(loadCfg){var um;if(um=this.getUpdater()){if(loadCfg&&loadCfg.renderer){um.setRenderer(loadCfg.renderer);delete loadCfg.renderer;}um.update.apply(um,arguments);}return this;},doAutoLoad:function(){this.load(typeof this.autoLoad=="object"?this.autoLoad:{url:this.autoLoad});}});Ext.reg("iframepanel",Ext.ux.ManagedIframePanel);Ext.ux.ManagedIframePortlet=Ext.extend(Ext.ux.ManagedIframePanel,{anchor:"100%",frame:true,collapseEl:"bwrap",collapsible:true,draggable:true,cls:"x-portlet"});Ext.reg("iframeportlet",Ext.ux.ManagedIframePortlet);Ext.apply(Ext.Element.prototype,{setVisible:function(visible,animate){if(!animate||!Ext.lib.Anim){if(this.visibilityMode==Ext.Element.DISPLAY){this.setDisplayed(visible);}else{if(this.visibilityMode==Ext.Element.VISIBILITY){this.fixDisplay();this.dom.style.visibility=visible?"visible":"hidden";}else{this[visible?"removeClass":"addClass"](String(this.visibilityMode));}}}else{var dom=this.dom;var visMode=this.visibilityMode;if(visible){this.setOpacity(0.01);this.setVisible(true);}this.anim({opacity:{to:(visible?1:0)}},this.preanim(arguments,1),null,0.35,"easeIn",function(){if(!visible){if(visMode==Ext.Element.DISPLAY){dom.style.display="none";}else{if(visMode==Ext.Element.VISIBILITY){dom.style.visibility="hidden";}else{Ext.get(dom).addClass(String(visMode));}}Ext.get(dom).setOpacity(1);}});}return this;},isVisible:function(deep){var vis=!(this.getStyle("visibility")=="hidden"||this.getStyle("display")=="none"||this.hasClass(this.visibilityMode));if(deep!==true||!vis){return vis;}var p=this.dom.parentNode;while(p&&p.tagName.toLowerCase()!="body"){if(!Ext.fly(p,"_isVisible").isVisible()){return false;}p=p.parentNode;}return true;}});Ext.onReady(function(){var CSS=Ext.util.CSS,rules=[];CSS.getRule(".x-managed-iframe")||(rules.push(".x-managed-iframe {height:100%;width:100%;overflow:auto;}"));CSS.getRule(".x-managed-iframe-mask")||(rules.push(".x-managed-iframe-mask{width:100%;height:100%;position:relative;}"));if(!CSS.getRule(".x-frame-shim")){rules.push(".x-frame-shim {z-index:9000;position:absolute;top:0px;left:0px;background:transparent!important;overflow:hidden;display:none;}");rules.push(".x-frame-shim-on{width:100%;height:100%;display:block;zoom:1;}");rules.push(".ext-ie6 .x-frame-shim{margin-left:5px;margin-top:3px;}");}CSS.getRule(".x-hide-nosize")||(rules.push(".x-hide-nosize,.x-hide-nosize object,.x-hide-nosize iframe{height:0px!important;width:0px!important;border:none;}"));if(!!rules.length){CSS.createStyleSheet(rules.join(" "));}});})();

/**
 * @class ExtMVC.KohiveViewportBuilder
 * @extends ExtMVC.ViewportBuilder
 * Creates the Kohive viewport layout
 */
ExtMVC.KohiveViewportBuilder = Ext.extend(ExtMVC.ViewportBuilder, {
  
  /**
   * Builds the Kohive viewport layout
   */
  build: function(os) {
    var config = os.getViewportBuilderConfig || {};
    
    // Apps
    os.applicationLauncher = new Kohive.views.os.ApplicationLauncher();
    
    // QuickApps
    os.quickAppsBar        = new Kohive.views.os.QuickAppsBar();
    
    // Hive
    os.hiveSummary         = new Kohive.views.hives.Summary();
    os.hiveOptions         = new Kohive.views.hives.Options({renderTo: Ext.getBody()});
    os.hivePanel           = new Kohive.views.hives.Index({renderTo: Ext.getBody()});
    
    // Member
    os.memberSummary       = new Kohive.views.members.Summary();
    os.memberOptions       = new Kohive.views.members.Options();
    os.memberPanel         = new Kohive.views.members.Index();
    
    // Header
    // os.widgetBar           = new Kohive.views.os.WidgetBar({});
    os.headerPanel         = new Kohive.views.os.HeaderPanel({});
    
    os.windowManager       = Ext.WindowMgr;
    
    // Viewport
    os.viewport            = new Ext.Viewport({
      monitorResize: true,
      layout:        'border',
      
      items: [
        {
          region:  'north',
          baseCls: '',
          cls:     '',
          height:  33,
          // height:  130, with widget bar
          items: [
            os.headerPanel// ,
            // os.widgetBar
          ]
        },
    				{
    					id:        'kohive_viewport_desktop',
    					region:    'center',
    					border:    false,
    				  bodyStyle: 'background-color: transparent;'
    				}
      ]
    });
    
    return os;
  }
});

ExtMVC.ViewportBuilderManager.register('kohive', ExtMVC.KohiveViewportBuilder);

/**
 * Ext.ux.HeaderToolbar
 * @extends Ext.Toolbar
 * Creates and manages a toolbar in the Header element of any Ext.Panel or subclass
 */
Ext.ux.HeaderToolbar = function(config) {
  var config = config || {};
  Ext.apply(this, config);
};

Ext.ux.HeaderToolbar.prototype = {
  /**
   * Add the header Toolbar to the Header element if a header is present and 
   * provide a getHeaderToolbar method
   * @param {Ext.Panel} The panel to add a header Toolbar to
   */
  init: function(panel) {
    Ext.apply(panel, {
      
      /**
       * Adds to Panel's onRender method, creates header toolbar if everything required is in place
       */
      onRender: panel.onRender.createSequence(function(ct, position) {
        // If it is a window we need to position the header toolbar in the window
        if (this.hbar && this.header) {
          this.headerToolbar = new Ext.Toolbar(this.hbar);
          
          // Find the element to render the toolbar to
          var toolbar = this.el.child('.x-window-tl').createChild({
            cls: 'x-window-toolbar-r',
            children: [
              { cls: 'x-window-toolbar-c' }
            ]
          });
          
          // get the toolbar element
          var el = this.el.child('.x-window-toolbar-c');
          
          // render the toolbar      
          this.headerToolbar.render(el);
          
          this.headerToolbar.addClass('x-header-toolbar');
          
          // Change the class on the toggle tool
          this.el.child('.x-tool-collapse-header').addClass('x-shown');
          
          return;
        };
        
        // If it is just a panel, we need to position it at the top of the panel
        if (this.hbar) {
          this.headerToolbar = new Ext.Toolbar(this.hbar);
          
          this.insert(0, this.headerToolbar);
          
          this.headerToolbar.addClass('x-header-toolbar');
        };
				
				this.addEvents(
					/**
					 * @event before-collapse-header-toolbar
					 * Fires before the header toolbar is collapsed
					 */
					'before-collapse-header-toolbar',
					
					/**
					 * @event before-expand-header-toolbar
					 * Fires before the header toolbar is expanded
					 */
					'before-expand-header-toolbar',
					
				  /**
				   * @event collapse-header-toolbar
				   * Fires when the header toolbar has been collapsed
				   */
				  'collapse-header-toolbar',
				
					/**
					 * @event expand-header-toolbar
					 * Fires when the header toolbar has been expaned
					 */
					'expand-header-toolbar'
				);
      }),
      
      /**
       * @return {Ext.Toolbar} The Header Toolbar, if one is available
       */
      getHeaderToolbar: function() {
        return this.headerToolbar;
      },
      
      /**
       * @property headerToolbarHidden
       * @type Boolean
       * Stores whether or not the header toolbar is currently hidden (collapsed)
       */
      headerToolbarHidden: false,
      
      /**
       * Collapses the header toolbar by sliding it upwards
       * @param {Object} opts Options object passed to the slideOut call
       */
      hideHeaderToolbar: function(opts) {
				this.fireEvent('before-collapse-header-toolbar', this.headerToolbar);
				
        var opts = opts || {};
        Ext.applyIf(opts, {
          scope:    this,
          duration: 0.3,
          callback: function() { 
            this.headerToolbarHidden = true;
            this.headerToolbar.hide();
						
						this.fireEvent('collapse-header-toolbar', this.headerToolbar);
          }
        });
        
        // Change the class on the toggle tool
        this.el.child('.x-tool-collapse-header').removeClass('x-shown');
        
        // Get the height of the headertoolbar
        this.height = {
          toolbar: this.headerToolbar.el.getHeight(),
          win:     this.el.getHeight()
        };
        
        this.headerToolbar.el.slideOut('t', opts);
        
        // Resize the shadow
        this.el.shadow.el.child('.xsc').shift({
          scope:    this,
          height:   this.el.shadow.el.child('.xsc').getHeight() - this.height.toolbar,
          duration: 0.3
        });
      },
      
      /**
       * Shows the header toolbar by sliding it downwards
       * @param {Object} opts Options object passed to the slideIn call
       */
      showHeaderToolbar: function(opts) {
				this.fireEvent('before-expand-header-toolbar', this.headerToolbar);
				
        var opts = opts || {};
        Ext.applyIf(opts, {
          scope:    this,
          duration: 0.3,
          callback: function() {
						this.headerToolbarHidden = false;
						
						this.fireEvent('expand-header-toolbar', this.headerToolbar);
					}
        });
        
        this.headerToolbar.show();
        this.headerToolbar.el.slideIn('t', opts);
        
        // Change the class on the toggle tool
        this.el.child('.x-tool-collapse-header').addClass('x-shown');
        
        // Resize the shadow
        this.el.shadow.el.child('.xsc').shift({
          scope:    this,
          height:   this.el.shadow.el.child('.xsc').getHeight() + this.height.toolbar,
          duration: 0.3
        });
        
        var newHeight = this.el.getHeight();
        if (newHeight != this.height.win) {
          this.el.shadow.el.shift({
            scope:    this,
            height:   this.el.shadow.el.getHeight() + this.height.toolbar,
            duration: 0.3
          });
        };
      },
      
      /**
       * Toggles the header toolbar between visible and hidden
       */
      toggleHeaderToolbar: function() {
        this.headerToolbarHidden ? this.showHeaderToolbar() : this.hideHeaderToolbar();
      }
    });
  }
};

/**
 * Ext.ux.HeaderToolbarButton
 * Customised Button for use in a HeaderPanel.
 * @extends Ext.Button
 */
Ext.ux.HeaderToolbarButton = function(config) {
  var config = config || {};
  
  Ext.ux.HeaderToolbarButton.superclass.constructor.call(this, config);
};

Ext.extend(Ext.ux.HeaderToolbarButton, Ext.Button, {
  template: new Ext.Template(
    '<div>',
      '<div class="x-btn-icon {icon} {iconPos}"></div>',
      '<div class="x-btn-text">',
        '{name}',
      '</div>',
    '</div>'
  ),
  
  /**
   * Override the default onRender method to update the styling of
   * the button and also to add icon functionality.
   */
  onRender: function(ct, position){
    // check if icon position is specified
    if (this.iconCls) {
      if (!this.iconPos) {
        this.iconPos = 'top';
      };
    };
    
    var btn, data = {
      name:     this.text,
      icon:     '' || this.iconCls,
      iconPos:  '' || this.iconPos
    };
    
    btn = this.template.append(ct, data, true);
    btn.unselectable();
    
    var btnEl = btn.child('div');
    
    this.initButtonEl(btn, btnEl);
    
    Ext.ButtonToggleMgr.register(this);
  },
  
  /**
   * Sets this button's text. Overrides superclass method to be compatible with our button template.
   * @param {String} text The button text
   */
  setText : function(text){
    this.text = text;
    if (this.el) {
      this.el.child(".x-btn-text").update(text);
    }
    this.autoWidth();
  }
});
Ext.reg('header_toolbar_button', Ext.ux.HeaderToolbarButton);

/**
 * @class Ext.for.HUD
 * @extends Ext.Panel
 * A special HUD panel.
 */
Ext.ux.HUD = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    baseCls: 'x-hud',
    width:   220,
    header:  true,
    defaults: {
      border:    false,
      bodyStyle: 'background:transparent'
    }
  });
  
  Ext.ux.HUD.superclass.constructor.call(this, config);
  
  this.on('show', this.addClickListenerToBody, this);
  this.on('hide', this.removeClickListenerToBody, this);
};

Ext.extend(Ext.ux.HUD, Ext.Panel, {
  /**
   * @property saving
   * Set to true while saving, stops window from being closed while a save is in progress
   */
  saving: false,
  
  /**
   * Renders the HTML for this component
   * @param {Ext.Container} ct The container to render this component to
   * @param {Number} position The position within the parent container to render this component to
   */
  onRender: function(ct, position) {
    var ct = ct || Ext.getBody();
    
    // Create the HUD
    this.el = ct.createChild({
      cls: this.baseCls + '-wrapper'
    });
    
    if (this.initialConfig.header === false) this.el.addClass('no-header');
    
    Ext.ux.HUD.superclass.onRender.apply(this, arguments);
  },
  
  // private
  initEvents: function() {
    Ext.ux.HUD.superclass.initEvents.apply(this, arguments);
    
    // Get the elements within the HUD
    this.bodyEl = this.el.child('.' + this.baseCls + '-body');
    
    // Set the height of the body to auto
    this.bodyEl.setHeight(this.initialConfig.height || 'auto');
    
    // Focus on the form
    this.focus();
    
    // Create the poker and anchor it
    if (this.initialConfig.poker !== false) this.pokerEl = this.createPoker();
  },
  
  /**
   * Shows the HUD and positions it
   * @param {Array} offset The offset
   * @param {String} position Something like 'b-t'
   */
  showHUD: function(offset, position, delay, callback, animate) {
    var tempTask = new Ext.util.DelayedTask(function() {
      if (animate === true) {
				this.show();
				this.el.hide();
				this.el.fadeIn({
					endOpacity: .8
				});
			} else {
				this.show();
			};

      this.positionHUD(offset || null, position || null);
      
      if (callback) callback.call(this);
    }, this);
    tempTask.delay(delay || 200);
  },
  
  /**
   * Method which positions the HUD accordingly
   * @param {Array} offset The offset
   * @param {String} position Something like 'b-t'
   */
  positionHUD: function(offset, position) {
    var offset   = offset   || [0, 0];
    var position = position || 'b-t';
    
    this.el.setHeight('auto');
    this.el.setHeight((this.pokerEl) ? this.el.getHeight() - this.pokerEl.getHeight() : 'auto');
    
    // Anchor the HUD to the hivePanel
    this.el.anchorTo(this.anchorToEl, position, offset);
    
    this.currentOffset = offset;
    
    // Get the correct poker position
    var pokerPosition = "t-b";
    var pokerOffset   = [0, 0];
    if (position == "b-t") { pokerPosition = "t-b"; };
    
    // Position the poker
    this.positionPoker(pokerOffset, pokerPosition);
    
    this.checkBoundaires();
    
    var obj = this;
    jQuery(window).bind('resize', function() {
      obj.checkBoundaires();
    });
  },
  
  /**
   * Checks if the HUD is out of view
   */
  checkBoundaires: function() {
    // Check if the HUD is out of bounds
    if (this.el.getWidth() + this.el.getX() > Ext.getBody().getWidth()) {
      this.moveToRight();
    };
  },
  
  /**
   * Moves the HUD over to the right for the member/hive panel
   */
  moveToRight: function() {
    var pokerPosition = "tr-br";
    var pokerOffset = [-10, 0];
    
    this.positionPoker(pokerOffset, pokerPosition, true);
    
    this.el.anchorTo(this.anchorToEl, 'br-tr', [this.currentOffset[0] - 5, this.currentOffset[1]]);
  },
  
  /**
   * Adds a click listener to the Ext.getBody element so when the user clicks out
   * of the HUD it closes.
   */
  addClickListenerToBody: function() {
    // Listener for body to check if the user clicks
    Ext.getBody().on('mousedown', function(e) {
      if (!e.getTarget('.x-hud-wrapper')  && !this.saving) {
        this.destroy();
      };
    }, this);
  },
  
  /**
   * Creates the poker element
   * @return {Ext.Element} The new poker
   */
  createPoker: function() {
    var ct = Ext.getBody();
    
    var poker = this.el.createChild({
      cls: 'x-hud-poker'
    });
    
    poker.setWidth(15);
    poker.setHeight(this.initialConfig.pokerHeight || 15);
    
    return poker;
  },
  
  /**
   * Creates the little sharp frakker at the bottom of the HUD
   * @param {Array} offset The offset
   * @param {String} position The position of where the poker should be. t-b/tr-br
   */
  positionPoker: function(offset, position, right) {
    if (!this.pokerEl) { return; };
    
    if (position == 'r-l') { this.pokerEl.addClass('x-left'); };
    
    this.pokerEl.anchorTo(this.el, position || 't-b', offset || [0, -5]);
  },
  
  /**
   * Adds a click listener to the Ext.getBody element so when the user clicks out
   * of the HUD it closes.
   */
  addClickListenerToBody: function() {
    // Listener for body to check if the user clicks
    Ext.getBody().on('mousedown', this.clickListener, this);
  },
  
  /**
   * Removes the click listener from the body
   */
  removeClickListenerToBody: function() {
    Ext.getBody().un('mousedown', this.clickListener, this);    
  },
  
  /**
   * Method for the click listener on the body
   */
  clickListener: function(e) {
    if (!e.getTarget('.x-hud-wrapper')) {
      this.hide();
    };
  }
});

Ext.reg('hud', Ext.ux.HUD);

/**
 * @class Ext.for.HUD
 * @extends Ext.FormPanel
 * A special HUD panel.
 */
Ext.form.HUD = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    baseCls: 'x-hud',
    width:   220,
    defaults: {
      border:    false,
      bodyStyle: 'background:transparent'
    }
  });
  
  Ext.form.HUD.superclass.constructor.call(this, config);
  
  this.addEvents(
    /**
     * @event save
     * Fires when the save button is clicked
     * @param {Ext.form.FormPanel} this The form panel which fired the event
     */
    'save',
        
    /**
     * @event cancel
     * Fires after the panel has been hidden.  Note that the data are still available in 
     * the form - it is just hidden
     * @param {Kohive.views.hives.New} this
     */
    'cancel'
  );
  
  this.on('show', this.addClickListenerToBody, this);
  this.on('hide', this.removeClickListenerToBody, this);
};

Ext.extend(Ext.form.HUD, Ext.FormPanel, {
  /**
   * @property saving
   * Set to true while saving, stops window from being closed while a save is in progress
   */
  saving: false,
  
  /**
   * Renders the HTML for this component
   * @param {Ext.Container} ct The container to render this component to
   * @param {Number} position The position within the parent container to render this component to
   */
  onRender: function(ct, position) {
    var ct = ct || Ext.getBody();
    
    // Create the HUD
    this.el = ct.createChild({
      cls: this.baseCls + '-wrapper'
    });
    
    if (this.initialConfig.header === false) this.el.addClass('no-header');
    
    Ext.form.HUD.superclass.onRender.apply(this, arguments);
  },
  
  // private
  initEvents: function() {
    Ext.form.HUD.superclass.initEvents.apply(this, arguments);
    
    // Get the elements within the HUD
    this.bodyEl = this.el.child('.' + this.baseCls + '-body');
    
    // Set the height of the body to auto
    this.bodyEl.setHeight('auto');
    
    // Focus on the form
    this.focus();
    
    // Create the poker and anchor it
    this.pokerEl = this.createPoker();
  },
  
  /**
   * Shows the HUD and positions it
   * @param {Array} offset The offset
   * @param {String} position Something like 'b-t'
   */
  showHUD: function(offset, position) {
    var tempTask = new Ext.util.DelayedTask(function() {
	
      this.show();
      this.positionHUD(offset || null, position || null);
    }, this);
    tempTask.delay(200);
  },
  
  /**
   * Method which positions the HUD accordingly
   * @param {Array} offset The offset
   * @param {String} position Something like 'b-t'
   */
  positionHUD: function(offset, position) {
    var offset   = offset   || [0, 0];
    var position = position || 'b-t';
    
    this.el.setHeight(this.el.getHeight() - this.pokerEl.getHeight());
    
    // Anchor the HUD to the hivePanel
    this.el.anchorTo(this.anchorToEl, position, offset);
    
    this.currentOffset = offset;
    
    // Get the correct poker position
    var pokerPosition = "t-b";
    var pokerOffset   = [0, 0];
    if (position == "b-t")   { pokerPosition = "t-b"; };
    
    // Position the poker
    this.positionPoker(pokerOffset, pokerPosition);
    
    this.checkBoundaires();
    
    var obj = this;
    jQuery(window).bind('resize', function() {
      obj.checkBoundaires();
    });
  },
  
  /**
   * Checks if the HUD is out of view
   */
  checkBoundaires: function() {
    // Check if the HUD is out of bounds
    if (this.el.getWidth() + this.el.getX() > Ext.getBody().getWidth()) {
      this.moveToRight();
    };
  },
  
  /**
   * Moves the HUD over to the right for the member/hive panel
   */
  moveToRight: function() {
    var pokerPosition = "tr-br";
    var pokerOffset = [-10, 0];
    
    this.positionPoker(pokerOffset, pokerPosition, true);
    
    this.el.anchorTo(this.anchorToEl, 'br-tr', [this.currentOffset[0] - 5, this.currentOffset[1]]);
  },
  
  /**
   * Adds a click listener to the Ext.getBody element so when the user clicks out
   * of the HUD it closes.
   */
  addClickListenerToBody: function() {
    // Listener for body to check if the user clicks
    Ext.getBody().on('mousedown', function(e) {
      if (!e.getTarget('.x-hud-wrapper')  && !this.saving) {
        this.destroy();
      };
    }, this);
  },
  
  /**
   * Updates save button text and disables it while saving
   */
  startSave: function() {
    this.disable();
    this.saving = true;
    
    // Find the form mask and create a loading animation child within it
    var mask = this.el.child('.ext-el-mask');
    if (mask) {
      mask.setHeight(this.el.getHeight() - 25);
      mask.setWidth(this.el.getWidth() - 3);
      mask.anchorTo(this.el, 'b-b', [1, -1]);
      
      this.loadingContainer = mask.createChild({
        cls: 'x-loading'
      });
      
      this.loadingContainer.anchorTo(mask, 'c-c', [0, -3]);
    };
  },

  /**
   * Re-enables button and resets text back to normal submit text
   */
  endSave: function() {
    if (this.loadingContainer) { this.loadingContainer.remove(); };
    
    this.enable();
    this.saving = false;
  },

  /**
   * Method which is called when the form is successfully submitted
   */
  onSuccess: function() {
    this.form.reset();
    this.destroy();
  },

  /**
   * Marks the appropriate errors on the panel
   * @param {Kohive.models.Hive} hive The hive model instance containing the errors 
   */
  onFailure: function(hive) {
    this.form.clearInvalid();
    this.form.markInvalid(hive.errors.forForm());
  },
  
  /**
   * Creates the poker element
   * @return {Ext.Element} The new poker
   */
  createPoker: function() {
    var ct = Ext.getBody();
    
    var poker = this.el.createChild({
      cls: 'x-hud-poker'
    });
    
    poker.setWidth(15);
    poker.setHeight(this.initialConfig.pokerHeight || 15);
    
    return poker;
  },
  
  /**
   * Creates the little sharp frakker at the bottom of the HUD
   * @param {Array} offset The offset
   * @param {String} position The position of where the poker should be. t-b/tr-br
   */
  positionPoker: function(offset, position, right) {
    if (!this.pokerEl) { return; };
    
    this.pokerEl.anchorTo(this.el, position || 't-b', offset || [0, -5]);
  },
  
  /**
   * Adds a click listener to the Ext.getBody element so when the user clicks out
   * of the HUD it closes.
   */
  addClickListenerToBody: function() {
    // Listener for body to check if the user clicks
    Ext.getBody().on('mousedown', this.clickListener, this);
  },
  
  /**
   * Removes the click listener from the body
   */
  removeClickListenerToBody: function() {
    Ext.getBody().un('mousedown', this.clickListener, this);    
  },
  
  /**
   * Method for the click listener on the body
   */
  clickListener: function(e) {
    if (!e.getTarget('.x-hud-wrapper')) {
      this.destroy();
    };
  }
});
Ext.reg('hud', Ext.form.HUD);

/**
 * Ext.ux.WindowSidePanel
 * @extends Ext.Toolbar
 * Creates and manages a sliding side panel for Ext.Windows
 */
Ext.ux.WindowSidePanel = function(config) {
  var config = config || {};
  Ext.apply(this, config);
};

Ext.ux.WindowSidePanel.prototype = {
  /**
   * @property headerToolbarHidden
   * @type Boolean
   * Stores whether or not the window panel is currently hidden (collapsed).
   * Set it to false if the window side panel should be displayed on render (defaults to true).
   */
  windowSidePanelHidden: true,
  
  /**
   * Add the side panel to the the window and provide a getWindowSidePanel method
   * @param {Ext.Window} The window to add a side panel to
   */
  init: function(win) {
    Ext.apply(win, {
      /**
       * Somehow I like to pass the plugin config directly via the plugin config object,
       * makes clear what plugin options can be set. Have to think about it though.
       */
      windowSidePanelHidden: this.windowSidePanelHidden,
      
      /**
       * Somehow I like to pass the plugin config directly via the plugin config object,
       * makes clear what plugin options can be set. Have to think about it though.
       */
      windowSidePanel: this.windowSidePanel,
      
      /**
       * Adds to Windows's onRender method, creates window panel if everything required is in place
       */
      onRender: win.onRender.createSequence(function(ct, position) {
        if (this.windowSidePanel || this.filterPanelConfig) {
          this.windowSidePanel = this.windowSidePanel || new Ext.ux.FilterPanel(this.filterPanelConfig);
          
          this.relayEvents(this.windowSidePanel, ['filters-updated']);
          
          // Create a side panel wrapper
          var wrapper = this.el.createChild({
            cls: 'x-window-side-panel-wrapper'
          });
          
          // Create the toggler
          this.toggler = wrapper.insertFirst({cls: 'x-window-side-panel-toggler'});
          
          // Render the panel into the window
          this.windowSidePanel.render(wrapper);
          
          // Create header and footer inside window panel
          this.windowSidePanel.el.insertFirst({cls: 'x-window-side-panel-header'});
          this.windowSidePanel.el.createChild({cls: 'x-window-side-panel-footer'});
          
          // create the shadows
          this.shadow = this.el.createChild({
            cls: 'x-shadow',
            children: [
              {
                cls: 'xst',
                children: [
                  { cls: 'xstl' },
                  { cls: 'xstc' },
                  { cls: 'xstr' }
                ]
              },
              {
                cls: 'xsc',
                children: [
                  { cls: 'xsml' },
                  { cls: 'xsmc' },
                  { cls: 'xsmr' }
                ]
              },
              {
                cls: 'xsb',
                children: [
                  { cls: 'xsbl' },
                  { cls: 'xsbc' },
                  { cls: 'xsbr' }
                ]
              }
            ]
          });
          
          this.on('render', this.anchorView, this);
          this.on('resize', this.resizeView, this);
          
          // In case we want to have the side panel open by default
          if (!this.windowSidePanelHidden)
            this.on('render', this.showWindowSidePanel, this);
          
          // Add click event to the toggler
          this.toggler.on('click', this.toggleWindowSidePanel, this);
					
					this.on({
						scope                            : this,
						'before-collapse-header-toolbar' : this.onHeaderToolbarCollapse,
						'before-expand-header-toolbar'   : this.onHeaderToolbarExpand
					});
        };
      }),
      
      /**
       * @return {Ext.Panel} The window panel, if one is available
       */
      getWindowSidePanel: function() {
        return this.windowSidePanel;
      },
      
      /**
       * Anchors the window panel to the window on first render
       */
      anchorView: function() {
        var tempTask = new Ext.util.DelayedTask(function() {
          // Resize the view
          this.resizeView();
        }, this);
        tempTask.delay(1000);
      },
      
      /**
       * Method which resizes the view according to the size of the window and also
       * anchors it to the window
       */
      resizeView: function() {
				// Show the panel before resizing
        this.windowSidePanel.show();
				
        // get the bwrap, which is what we want to set the height attribute to
        var el      = this.windowSidePanel.el.child('.x-window-side-panel-bwrap');
				var childEl = el.child('.x-window-side-panel-body');
				
        // Set the height of the side window panel and anchor the side panel to the window
        el.setHeight(this.el.getHeight() - 90);
        childEl.setHeight(el.getHeight() + 9);
        this.windowSidePanel.el.anchorTo(this.el, 'r-l', [0, 15]);
				
				this.currentWindowSidePanelHeight = el.getHeight();
        
        // anchor the toggler
        if (this.windowSidePanelHidden) {
          this.toggler.anchorTo(this.el, 'r-l');
          
          // hide the panel again
          this.windowSidePanel.hide();
        } else {
          this.toggler.anchorTo(this.windowSidePanel.el, 'r-l');
        };
      },
			
			/**
			 * Called when the header toolbar is collapsed
			 */
			onHeaderToolbarCollapse: function() {
				var el      = this.windowSidePanel.el.child('.x-window-side-panel-bwrap');
				var childEl = el.child('.x-window-side-panel-body');
				
				var top    = parseFloat(this.windowSidePanel.el.dom.style.top.slice(0, this.windowSidePanel.el.dom.style.top.length - 2));
				var height = (this.windowSidePanelHidden) ? this.currentWindowSidePanelHeight : el.getHeight();
				
				this.windowSidePanel.el.shift({
					duration: 0.3,
					top:      top + 59
				});
				
				el.shift({
					duration: 0.3,
					height:   height - 59
				});
				
				childEl.shift({
					duration: 0.3,
					height:   height - 50
				});
			},
			
			/**
			 * Called when the header toolbar is expanded
			 */
			onHeaderToolbarExpand: function() {
				var el      = this.windowSidePanel.el.child('.x-window-side-panel-bwrap');
				var childEl = el.child('.x-window-side-panel-body');
				
				var top    = parseFloat(this.windowSidePanel.el.dom.style.top.slice(0, this.windowSidePanel.el.dom.style.top.length - 2));
				var height = (this.windowSidePanelHidden) ? this.currentWindowSidePanelHeight - 59 : el.getHeight();
				
				this.windowSidePanel.el.shift({
					duration: 0.3,
					top:      top - 59
				});
				
				el.shift({
					duration: 0.3,
					height:   height + 59
				});
				
				childEl.shift({
					duration: 0.3,
					height:   height + 68
				});
			},
      
      /**
       * Collapses the window panel by sliding it to the right behind the window
       * @param {Object} opts Options object passed to the slideOut call
       */
      hideWindowSidePanel: function(opts) {
        var opts = opts || {};
        Ext.applyIf(opts, {
          scope:    this,
          duration: 0.3,
          callback: function() { 
            this.windowSidePanelHidden = true;
            this.windowSidePanel.hide();
          }
        });
        
        this.windowSidePanel.el.slideOut('r', opts);
        
        // shift the toggler
				this.toggler.shift({
					left:     -23,
					duration: 0.3
				});
        
        this.toggler.removeClass('x-toggled');
      },
      
      /**
       * Shows the window panel by sliding it to the left from behind the window
       * @param {Object} opts Options object passed to the slideIn call
       */
      showWindowSidePanel: function(opts) {
        var opts = opts || {};
        Ext.applyIf(opts, {
          scope:    this,
          duration: 0.3,
          callback: function() {this.windowSidePanelHidden = false;}
        });
        
        this.windowSidePanel.show();
          
        this.windowSidePanel.el.slideIn('r', opts);
        
        // shift the toggler
				this.toggler.shift({
					left:     -163,
					duration: 0.3
				});
        
        this.toggler.addClass('x-toggled');
      },
      
      /**
       * Toggles the window panel between visible and hidden
       */
      toggleWindowSidePanel: function() {
        this.windowSidePanelHidden ? this.showWindowSidePanel() : this.hideWindowSidePanel();
      }
    });
  }
};

/**
 * @class Ext.ux.FilterPanel
 * @extends Ext.Panel
 * Renders a filter panel suitable for attachment to a Kohive.Window via a WindowSidePanel
 * 
 * @cfg filters
 * @type Array
 * An array of filter configuration objects. This should contain at least the following:
 * <ul>
 *   <li>title: The title of the filter, rendered as a heading</li>
 *   <li>iconCls: The CSS class used to render the icon for items in this filter</li>
 *   <li>store: The Ext.data.Store subclass to retrieve data from</li>
 *   <li>convert: A function used with store.each to retrieve the values from the store</li>
 * </ul>
 * Defaults to an empty array
 */
Ext.ux.FilterPanel = Ext.extend(Ext.Panel, {

  constructor: function(config) {
    config = config || {};
    
    Ext.applyIf(config, {
       filters: [],
       
       baseCls: 'x-window-side-panel',
       border:  false,
       header:  false,
       defaults: {
         border: false
       }
    });
    
    Ext.ux.FilterPanel.superclass.constructor.call(this, config);
    
    Ext.apply(this, {
      /**
       * @property currentlyApplied
       * @type Object
       * The valueField values of all currently ticked filter items, split by filter.
       * e.g. {'tags': ['cat, 'dog'], 'authors': [1, 5, 8]}
       */
      currentlyApplied: {}
    });
    
    this.initEvents();
    this.setupFilters();
    this.initListeners();
  },
  
  /**
   * Renders the filters into the panel's body
   */
  renderFilters: function() {
    this.template.overwrite(this.body, {filters: this.filters});
    
    //re-tick boxes as appropriate after rendering
    for (var name in this.currentlyApplied) {
      this.setCurrentlyApplied(name, this.currentlyApplied[name], false);
    }
  },
  
  /**
   * Instantiates the filter config objects into Ext.ux.Filter subclass objects
   */
  setupFilters: function() {
    var filters = [];
    
    Ext.each(this.filters, function(filterConfig) {
      filters.push(Ext.ux.FilterManager.build(filterConfig.ftype, filterConfig));
    }, this);
    
    this.filters = filters;
  },
    
  /**
   * Returns all currently applied (ticked) filter items
   * @return {Object} All currently applied filter items
   */
  getCurrentlyApplied: function() {
    return this.currentlyApplied;
  },
  
  /**
   * @property template
   * @type Ext.XTemplate
   * The XTemplate used to create each filter
   */
  template: new Ext.XTemplate(
    '<ul class="filters many">',
      '<tpl for="filters">',
        '<li<tpl if="this.isEmpty(options)"> class="x-empty"</tpl>>',
          '<h3>{title}</h3>',
          '<ul class="filter-options none" id="filter-options-{name}">',
            '<tpl for="options">',
							'<tpl if="text">',
              '<li class="filter-option {iconCls}<tpl if="!iconCls">{parent.name}</tpl>" id="filter-option-{parent.name}-{id}">',
								'<div class="x-icon"></div>',
                '<div class="filter-options-hidden"></div>',
                '<span class="text">{text}</span>',
              '</li>',
	            '</tpl>',
            '</tpl>',
          '</ul>',
        '</li>',
      '</tpl>',
    '</ul>', {
		isEmpty: function(options) {
			if (!options || !options.length) return true;
			if (options.length == 0) return true;
			return false;
		}
  }),
    
  /**
   * Called when the body element is clicked - fires events appropriately
   */
  onClick: function(e, element) {
    var el      = Ext.get(element),
        liEl    = e.getTarget('li.filter-option', 2),
        h3El    = e.getTarget('h3', 2),
        inputEl = e.getTarget('div', 2);
        spanEl  = e.getTarget('span', 2);
    
    switch (element) {
      case liEl:
      // case spanEl:  this.onLiClick(liEl);    break;
      case spanEl:  this.onInputClick(el);   break;
      case h3El:    this.onHeadingClick(el); break;
      case inputEl: this.onInputClick(el);   break;
    }
  },
  
  /**
   * Called when the user mouses over the body.  Adds a 'hover' class to the appropriate <li>
   * @param {Ext.EventObject} e The Event Object
   */
  onMouseover: function(e) {
    var li = Ext.get(e.getTarget('li.filter-option', 2));
        
    if (li) li.addClass('x-over');
  },
  
  /**
   * Called when the mouse leaves the body element.  Removes class 'hover' from all <li> elements
   * @param {Ext.EventObject} e The Event object
   */
  onMouseout: function(e) {
    var ul = this.body.select('li').removeClass('x-over');
  },
  
  /**
   * Called when a heading element is clicked.  Default behaviour is to collapse the associated ul
   * @param {Ext.Element} heading The Ext.Element for the heading that was clicked
   */
  onHeadingClick: function(heading) {
    var animConfig = {
      scope:    this,
      remove:   false,
      duration: 0.3,
      callback: function() {
        //create a closure to keep a reference to the correct heading
        return function() {
          heading.toggleClass('heading-collapsed');
        };
      }()
    };
    
    var ul     = heading.parent().child('ul'),
        action = ul.isVisible() ? 'slideOut' : 'slideIn';
    
    ul.enableDisplayMode();
    ul[action]('t', animConfig);
  },
  
  /**
   * Called when an <li> element is clicked.  Default behaviour is to mark the filter item this
   * represents as the only one applied, and to untick all others
   * @param {Ext.Element} li The <li> element that was clicked
   */
  onLiClick: function(li) {
    var options = this.parseFilterId(li.id),
        app     = this.currentlyApplied[options.name] || [];
    
    // reset options of all types, max. one option can be selected
    this.currentlyApplied = {};
		
    this.setCurrentlyApplied(options.name, [options.id]);
  },
  
  /**
   * Called when a checkbox is clicked.  By default this marks the item the checkbox represents
   * as being applied, but does not un-mark any other items
   * @param {Ext.Element} input The checkbox that was clicked
   */
  onInputClick: function(input) {
    var options = this.parseFilterId(input.parent().id),
        app     = this.currentlyApplied[options.name] || [];
    
    //check if the option has an id
    if (!options.id) return;
    
    app.indexOf(options.id) == -1 ? app.push(options.id) : app.remove(options.id);
    
    this.setCurrentlyApplied(options.name, app);
  },
  
  /**
   * Sets the list of currently applied items.
   * Ticks or unticks the appropriate checkboxes
   * @param {String} name The name of the filter to set applied IDs for
   * @param {Array} ids The ids of the items currently applied
   * @param {Boolean} firesEvent False to cancel firing on the filters-updated event (defaults to true)
   */
  setCurrentlyApplied: function(name, ids, firesEvent) {
    var numOfCurrentlyApplied = 0;
    
    this.currentlyApplied[name] = ids || [];
    
    // loop through all filter options of all types
    this.el.select('li.filter-option').each(function(liEl) {
      var filterOption = this.parseFilterId(liEl.dom.id);
      
      // remove selection marker from filter option
      liEl.removeClass('x-selected');
      liEl.child('div').removeClass('visible');
      
      // add selection marker to filter option if currently selected
      if (this.currentlyApplied[filterOption.name] != undefined && this.currentlyApplied[filterOption.name].indexOf(filterOption.id) >= 0) {
        liEl.addClass('x-selected');
        liEl.child('div').addClass('visible');
        numOfCurrentlyApplied++;
      }
    }, this);
    
    // add class to the parent ul of all filter types depending on the amount of selected items
    // if (numOfCurrentlyApplied > 1)
      // this.el.child('ul.filters').removeClass('many');
      // this.el.child('ul.filters').addClass('many');
    // else
      // this.el.child('ul.filters').removeClass('many');
    
    // loop through each of the filters and check if any are empty
    for (filter in this.currentlyApplied) {
      if (this.currentlyApplied[filter].length == 0) delete this.currentlyApplied[filter];
    };
    
    if (firesEvent !== false) this.fireEvent('filters-updated', this.getCurrentlyApplied(), (name == "hashs") ? true : false);
  },
  
  /**
   * Parses the ID string of a filter <li> node into a filter name and item ID. This is used
   * to parse name and ID from a <li> element inside this.template
   * @param {String} id The ID of the filter element that was clicked
   * @return {Object} An object containing the filter name and id of the filtered item
   */
  parseFilterId: function(id) {
    var els  = id.split('-'),
        name = els[2].split('.')[0];
    
    return {
      name: name,
      id:   (els[4]) ? els[3] + "-" + els[4] : els[3]
    };
  },
	
	/**
	 * Checks the width of the text of all child span.text elements and adds ellipsis to them
	 * iff nessecary
	 */
	initEllipsis: function() {
		var maxWidth = 70;
		
		// Get each of the elements
		var els = this.el.select('span.text');
		
		// Loop through each of the span childs
		els.each(function(el) {
			var text = el.dom.innerHTML;
			var size = Ext.util.TextMetrics.measure(el, text);
			var gotIt = false;
			
			if (size.width > maxWidth) {
				for (var i = 0; i < text.length; i++) {
					if (gotIt) return;
					
					var newString = text.slice(0, text.length - i) + '...';
					var newSize   = Ext.util.TextMetrics.measure(el, newString);
					
					if (maxWidth > newSize.width) {
						gotIt = true;
						
						el.dom.innerHTML = newString;
					};
				};
			};
		}, this);
	},
  
  /**
   * Listens to load events for each filter's store and refreshes the filter accordingly
   */
  initListeners: function() {
    Ext.each(this.filters, function(filter) {
      filter.on('refreshed', this.renderFilters, this);
      filter.on('refreshed', this.initEllipsis,  this);
    }, this);
    
    this.on('refreshed', this.renderFilters, this);
    this.on('refreshed', this.initEllipsis,  this);
		
    //attach click listeners once the panel has been rendered
    this.on('render', function() {
      this.renderFilters();
      
      this.body.on({
        scope    : this,
        click    : this.onClick,
        mouseover: this.onMouseover,
        mouseout : this.onMouseout
      });
    }, this);
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event filters-updated
       * Fired whenever the user applies a new filter option
       * @param {Object} currentlyApplied An object where the keys are the filter names and each
       * value is an array of the IDs currently selected
       */
      'filters-updated'
    );
  }
});

Ext.reg('filter_panel', Ext.ux.FilterPanel);

/**
 * @class Ext.ux.FilterManager
 */
Ext.ux.FilterManager = function() {
  return {
    /**
     * @property types
     * @type Object
     * The stored filter types
     */
    types: {},
    
    /**
     * Registers a FilterType by name
     * @param {String} name The string name of the filter type to register
     * @param {Function} filter A reference to the filter's constructor
     */
    reg: function(name, filter) {
      this.types[name] = filter;
    },
    
    /**
     * Finds a given filter by registered name
     * @param {String} name The name of the ftype
     * @return {Ext.ux.Filter|null} The filter's constructor, or null
     */
    find: function(name) {
      return this.types[name];
    },
    
    /**
     * Constructs a filter based on ftype and a config object
     * @param {String} name The name of the ftype to construct a filter instance of
     * @return {Ext.ux.Filter} The newly constructed filter
     */
    build: function(name, config) {
      var con = this.find(name);
      
      if (typeof con != 'undefined') return new con(config);
    }
  };
}();

/**
 * @class Ext.ux.Filter
 * @extends Ext.util.Observable
 * Filter base class - extend this to create your own filter
 */
Ext.ux.Filter = Ext.extend(Ext.util.Observable, {
	
  constructor: function(config) {
    Ext.ux.Filter.superclass.constructor.apply(this, arguments);
    
    Ext.applyIf(config, {
      title  : config.name,
      options: []
    });
		
    Ext.apply(this, config);
    // this.initListeners();
    this.initEvents();
    
    // this.refresh();
  },
  
  refresh: Ext.emptyFn,
  
  /**
   * Fired when an event is received from the listenTo component.  By default this simply calls
   * this.refresh() on every event.  Override in a subclass to alter the behaviour
   */
  // eventReceived: function() {
  //   this.refresh.apply(this, arguments);
  // },
  
  /**
   * Sets up listeners if any are specified in configuration.  All events specified
   * simply cause this.eventReceived() to be invoked
   */
  // initListeners: function() {
  //   if (typeof this.listenTo == 'object') {
  //     Ext.each(this.listenTo.events || [], function(listener) {
  //       this.listenTo.component.on(listener, this.eventReceived, this);
  //     }, this);
  //   }
  //   
  //   if (typeof this.store != 'undefined') this.store.on('load', this.refresh, this);
  // },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event refreshed
       * Fires every time the options in this Filter are updated
       * @param {Ext.ux.Filter} filter This filter instance
       */
      'refreshed'
    );
  },
	
	/**
	 * Function for Array.sort() which sorts the field alphabetically
	 */
	sortAlphabetically: function(a, b) {
	  if (!a || !a.text || !b || !b.text) return 0;
	  
		var nameA = a.text.toLowerCase();
		var nameB = b.text.toLowerCase();
		if (nameA < nameB) return -1;
		if (nameA > nameB) return 1;
		return 0;
	}
});

/**
 * @class Ext.ux.Filter.Authors
 * @extends Ext.ux.Filter
 * A configurable filter for filtering tags on a given view.
 * Requests tag data for the current object from the server
 */
Ext.ux.Filter.Authors = Ext.extend(Ext.ux.Filter, {
  
  constructor: function() {
    Ext.ux.Filter.Authors.superclass.constructor.apply(this, arguments);
    
		this.store = new Ext.data.JsonStore({
      root:   'authors',
      url:    '/authors/for',
      fields: ['author_membership_id', 'author_nickname']
    });
		
    Ext.applyIf(this, {
      title:                      Ext.ux.Translator.translate('authors'),
      name:                       'authors',
      iconCls:                    'author'
    });
  },
	
	/**
   * Overrides the default eventReceived.  If the first argument is a model instance the
   * store's url is updated to match the URL for this particular instance, and the store
   * is reloaded.  If not a model instance, this just refreshes.
   * @param {Ext.data.Store} store The store full of records
   */
  eventReceived: function(store) {
    //TODO: this is not a good way of doing this test, but instanceof won't work until
    //MVC 0.6, at which point we can use instance instanceof Ext.data.Record
    if (store && store.data && store.data.items && store.data.items[0] && typeof store.data.items[0].newRecord != 'undefined') {
			var	instance = store.data.items[0];

			this.store.proxy.conn.url = this.urlFor(instance);

			this.store.reload({
				scope:    this,
				callback: this.refresh
			});

			//keep a reference to the currently loaded instance so as not to refresh too often
			this.currentModelInstance = instance;
		} else if (typeof store.newRecord != 'undefined') {
			this.store.proxy.conn.url = this.urlFor(store);
       
			this.store.reload({
				scope:    this,
				callback: this.refresh
			});

			//keep a reference to the currently loaded instance so as not to refresh too often
			this.currentModelInstance = store;
    } else {
      this.refresh(store);
    };
  },
	
  refresh: function(store) {
    this.options = [];
		var authors = [];
		
		// if (store && store.totalLength == 0) {
		// 	return this.fireEvent('refreshed', this);
		// };
		
		if (!this.store) return;
		
		// Don't allow duplicates
		this.store.each(function(item) {
			if (!item.get('author_membership_id')) return;
			
			var obj = {
				id:   item.get('author_membership_id'),
	      text: item.get('author_nickname')
			};
			
			var inIt = false;
			
			for (var i = 0; i < authors.length; i++) {
				if (authors[i].id == obj.id) inIt = true;
			};
			
			if (!inIt) authors.push(obj);
    }, this);

    Ext.each(authors, function(author) {
      this.options.push({
        id:      author.id,
        text:    author.text,
        iconCls: this.iconCls
      });
    }, this);
		
		this.options.sort(this.sortAlphabetically);
    
    this.fireEvent('refreshed', this);
  },
  
  /**
   * Builds the url to retrieve tags from for a given model instance
   * @param {ExtMVC.Model.Base} instance The model instance
   * @return {String} The url for this instance
   */
  urlFor: function(instance) {
		var className = instance.className;
		
    var id = (className == "Image") ? instance.get('album_id') : Kohive.OS.getCurrentHiveId();
    var url = (this.name == "recipients") ? "recipients" : "authors";

		if (className == "File") { className = "FileCabinet"; };
		
    return String.format("/{0}/for/{1}/{2}", url, className, id);
  }
});

Ext.ux.FilterManager.reg('authors', Ext.ux.Filter.Authors);

/**
 * @class Ext.ux.Filter.Hashs
 * @extends Ext.ux.Filter
 * A configurable filter for filtering tags on a given view.
 * Requests tag data for the current object from the server
 */
Ext.ux.Filter.Hashs = Ext.extend(Ext.ux.Filter, {
  /**
   * 
   */
  constructor: function() {
    Ext.ux.Filter.Hashs.superclass.constructor.apply(this, arguments);
    
    Ext.applyIf(this, {
      title:   Ext.ux.Translator.translate('hash_tags'),
      name:    'hashs',
      iconCls: 'hash'
    });
  },
  
  /**
   * @property hashtagRegExp
   * @type RegExp
   */
  hashtagRegExp: new RegExp('#[a-z_\-]+[0-9_\-]*[a-z0-9_\-]+', 'gi'),
  
  clear: function() {
    this.options = [];
    this.optionsPlain = [];
  },
  
  /**
   * 
   */
  refresh: function() {
    this.options = [];
    this.optionsPlain = [];
		
    this.listenTo.component.each(function(record) {
      //get the body of the record
      var body = record.get('body');
      
      //find the matches for the hashtag regexp
      var matches = body.match(this.hashtagRegExp);
      
      Ext.each(matches || [], function(match) {
        //check if the hashtag has already been added
        if (this.optionsPlain.indexOf(match) != -1) return;
        
        //push only the match to an array so we can check again above
        this.optionsPlain.push(match);
        
        //push the proper match..
        this.options.push({
          id:      match.split('#')[1],
          text:    match.split('#')[1],
          iconCls: this.iconCls
        });
      }, this);
    }, this);
		
    this.options.sort(this.sortAlphabetically);
    
    // this.fireEvent('refreshed', this);
  }
});

Ext.ux.FilterManager.reg('hashs', Ext.ux.Filter.Hashs);

/**
 * @class Ext.ux.Filter.Milestones
 * @extends Ext.ux.Filter
 */
Ext.ux.Filter.Milestones = Ext.extend(Ext.ux.Filter, {
  /**
   * 
   */
  constructor: function() {
    Ext.ux.Filter.Milestones.superclass.constructor.apply(this, arguments);
    
    Ext.applyIf(this, {
      title:   Ext.ux.Translator.translate('milestones'),
      name:    'milestones',
      iconCls: 'milestone'
    });
  },
  
  /**
   * 
   */
  refresh: function() {
    this.options = [];
    this.optionsPlain = [];
    
    //get the controller + the milestones from it
    var controller = Kohive.OS.getController('tasks');
    
    controller.on('milestones', function(milestones) {
      //return if no milestones
      if (!milestones) return;
      
      milestones.each(function(milestone) {
        //check if the milestone has already been added
        if (this.optionsPlain.indexOf(milestone.get('id')) != -1) return;
        
        //push only the match to an array so we can check again above
        this.optionsPlain.push(milestone.get('id'));
        
        //push the proper match..
        this.options.push({
          id:      milestone.get('id'),
          text:    milestone.get('title'),
          iconCls: this.iconCls
        });
      }, this);
    }, this);
    
    controller.getMilestones();
    
    // this.options.sort(this.sortAlphabetically);
    
    // this.fireEvent('refreshed', this);
  }
});

Ext.ux.FilterManager.reg('milestones', Ext.ux.Filter.Milestones);

/**
 * @class Ext.ux.Filter.states
 * @extends Ext.ux.Filter
 * A configurable filter for filtering states on a given view.
 * Requests state data for the current object from the server
 */
Ext.ux.Filter.states = Ext.extend(Ext.ux.Filter, {
  
  constructor: function() {
    this.store = new Ext.data.JsonStore({
      root:   'states',
      url:    '/states/for',
      fields: ['name', 'id']
    });
    
    Ext.ux.Filter.states.superclass.constructor.apply(this, arguments);
    
    Ext.applyIf(this, {
      title:   Ext.ux.Translator.translate('state'),
      name:    'states',
      iconCls: 'state'
    });
  },
  
  /**
   * Overrides the default eventReceived.  If the first argument is a model instance the
   * store's url is updated to match the URL for this particular instance, and the store
   * is reloaded.  If not a model instance, this just refreshes.
   * @param {Ext.data.Store} store The store full of records
   */
  eventReceived: function(store) {
    // console.log('eventReceived')
    // debugger
    //TODO: this is not a good way of doing this test, but instanceof won't work until
    //MVC 0.6, at which point we can use instance instanceof Ext.data.Record
    if (store && store.data && store.data.items && store.data.items[0] && typeof store.data.items[0].newRecord != 'undefined') {
			var	instance = store.data.items[0];

			this.store.proxy.conn.url = this.urlFor(instance);

			this.store.reload({
				scope:    this,
				callback: this.refresh
			});

			//keep a reference to the currently loaded instance so as not to refresh too often
			this.currentModelInstance = instance;
		} else if (typeof store.newRecord != 'undefined') {
			this.store.proxy.conn.url = this.urlFor(store);
       
			this.store.reload({
				scope:    this,
				callback: this.refresh
			});

			//keep a reference to the currently loaded instance so as not to refresh too often
			this.currentModelInstance = store;
    } else {
      this.refresh();
    };
  },

  refresh: function() {
    this.options = [];
    
    var states = [];
		
		if (!this.store) return;
		
    //build a unique array of states
    this.store.each(function(state) {
      if (states.indexOf(state.data.name) == -1) states.push(state.data.name);
    });
		
		//add stuf so the xttemplate doesnt die in filterpanel.js
		this.options[0] = {text:''};
		this.options[1] = {text:''};
		this.options[2] = {text:''};
		this.options[3] = {text:''};
		this.options[4] = {text:''};
		this.options[5] = {text:''};
		this.options[6] = {text:''};
		this.options[7] = {text:''};
		this.options[8] = {text:''};
		
    Ext.each(states, function(state) {
			var text = state,
			    cls  = "";
			
			//change the numbers into translations
			if (this.name == "effort" || this.name == "priority") {
				if (text === 1) text = Ext.ux.Translator.translate('app_tasks_low');
				if (text === 2) text = Ext.ux.Translator.translate('app_tasks_medium');
				if (text === 3) text = Ext.ux.Translator.translate('app_tasks_high');
			};
			
			if (this.name == "flagged") {
			  if (!text) text = false;
			  if (!state) state = false;
			  
				if (text === true) {
				  text = Ext.ux.Translator.translate('app_tasks_flagged');
				  cls = "true";
				} else {
				  text = Ext.ux.Translator.translate('app_tasks_not_flagged');
				  cls = "false";
				};
			};
			
			if (this.name == "type") {
				if (text === 0) { text = Ext.ux.Translator.translate('app_tasks_null'); cls = "null"; };
				if (text === 1) { text = Ext.ux.Translator.translate('app_tasks_component'); cls = "cube"; };
				if (text === 2) { text = Ext.ux.Translator.translate('app_tasks_feature'); cls = "star"; };
				if (text === 3) { text = Ext.ux.Translator.translate('app_tasks_critical_bug'); cls = "spider"; };
				if (text === 4) { text = Ext.ux.Translator.translate('app_tasks_bug'); cls = "bug"; };
				if (text === 5) { text = Ext.ux.Translator.translate('app_tasks_chore'); cls = "brush"; };
				if (text === 6) { text = Ext.ux.Translator.translate('app_tasks_admin'); cls = "console"; };
				if (text === 7) { text = Ext.ux.Translator.translate('app_tasks_tweak'); cls = "tool"; };
				if (text === 8) { text = Ext.ux.Translator.translate('app_tasks_design'); cls = "colors"; };
			};
			
			//if the filter is state change the text into a translation
			if (this.name == "state") {
				if (text == "pending") text = Ext.ux.Translator.translate('app_tasks_started');
				else if (text == "new") text = Ext.ux.Translator.translate('app_tasks_pending');
				else text = Ext.ux.Translator.translate('app_tasks_' + text);
			};
			
			var iconCls = (this.name == "state") ? this.name + " " + state : this.name + " " + text;
			if (cls != "") iconCls = this.name + " " + cls;
			
			var obj = {
        id:      state,
        text:    text,
        iconCls: iconCls
      };
			
			if (this.name == "effort" || this.name == "priority") {
				if (state === 1) this.options[0] = obj;
				if (state === 2) this.options[1] = obj;
				if (state === 3) this.options[2] = obj;
		  } else if (this.name == "flagged") {
		    if (!state) this.options[1] = obj;
				if (state === true) this.options[0] = obj;
				if (state === false) this.options[1] = obj;
			} else if (this.name == "state") {
				if (text === "overdue") this.options[0] = obj;
				if (text === "pending") this.options[1] = obj;
				if (text === "started") this.options[2] = obj;
				if (text === "completed") this.options[3] = obj;
				if (text === "invalid") this.options[4] = obj;
			} else if (this.name == "type") {
				if (!state) this.options[0] = obj;
				if (state === 0) this.options[0] = obj;
				if (state === 1) this.options[1] = obj;
				if (state === 2) this.options[2] = obj;
				if (state === 3) this.options[3] = obj;
				if (state === 4) this.options[4] = obj;
				if (state === 5) this.options[5] = obj;
				if (state === 6) this.options[6] = obj;
				if (state === 7) this.options[7] = obj;
				if (state === 8) this.options[8] = obj;
			} else {
	      this.options.push(obj);
			};
    }, this);
    
    this.fireEvent('refreshed', this);
  },
  
  /**
   * Builds the url to retrieve states from for a given model instance
   * @param {ExtMVC.Model.Base} instance The model instance
   * @return {String} The url for this instance
   */
  urlFor: function(instance) {
		var id = Kohive.OS.getCurrentHiveId();
		var className = instance.className;
		
		return String.format("/{0}/for/{1}/{2}", this.name, className, id);
  }
});

Ext.ux.FilterManager.reg('states', Ext.ux.Filter.states);

/**
 * @class Ext.ux.Filter.StoreHelper
 * @extends Ext.ux.Filter
 */
Ext.ux.Filter.StoreHelper = Ext.extend(Ext.ux.Filter, {
  /**
   * 
   */
  constructor: function() {
    Ext.ux.Filter.StoreHelper.superclass.constructor.apply(this, arguments);
    
    Ext.applyIf(this, {
      name:         this.valueField,
      displayField: this.valueField,
      iconCls:      ""
    });
  },

  clear: function() {
    this.options = [];
    this.optionsPlain = [];
  },
  
  /**
   * 
   */
  // refresh: function(currentStore, updatedRecord, method, name) {
  refresh: function(record, updatedRecord, method, name) {
    // this.options = [];
    // this.optionsPlain = [];
		
		//check if it was toggled
		if (name == "toggled") return;
		
    // var store = this.view.originalStore;
    // if (!store) return;
		
    // Ext.each(store, function(record) {
      //check if it is a store update..
  		if (updatedRecord && updatedRecord.get && method && method == Ext.data.Record.EDIT) {
        if (updatedRecord.get('id') == record.get('id')) {
          record = updatedRecord;
        };
  		};
      
      var valueField   = record.data[this.valueField],
          displayField = record.data[this.displayField];
      
      //check if the value/display field has a . (which means it is a child)
      if (this.valueField.split('.').length > 1 || this.displayField.split('.').length > 1) {
        var container    = this.valueField.split('.')[0],
            valueField   = this.valueField.split('.')[1],
            displayField = this.displayField.split('.')[1],
            isObject     = (container[container.length - 1] == "s") ? false : true;
        
        if (isObject) {
          if (!record.data[container]) return;
          
          var id   = record.data[container][valueField],
              text = record.data[container][displayField];
          
          this.addOption(id, text);
        } else {
          var array = [],
              dataArray = record.data[container] || [];
          
          Ext.each(dataArray, function(tag) {
            var id   = tag[valueField],
                text = tag[displayField];
            
            this.addOption(id, text, 'tag');
          }, this);
        };
      } else {
        //get the body of the record
        var id   = valueField,
            text = displayField || id,
            cls  = "";
        
        //change the numbers into translations
        if (this.name == "effort" || this.name == "priority") {
          if (text === "low")    text = Ext.ux.Translator.translate('app_tasks_low');
          if (text === "medium") text = Ext.ux.Translator.translate('app_tasks_medium');
          if (text === "high")   text = Ext.ux.Translator.translate('app_tasks_high');
        };
        
        if (this.name == "flagged") {
          if (!text)           { text = false; id = false; };
          if (text === "true") { text = true;  id = true; };
          
          if (text === true) {
            text = Ext.ux.Translator.translate('app_tasks_flagged');
            cls = "true";
          } else {
            text = Ext.ux.Translator.translate('app_tasks_not_flagged');
            cls = "false";
          };
        };
        
        if (this.name == "type") {
          if (text === "null")    { text = Ext.ux.Translator.translate('app_tasks_null'); cls = "null"; };
          if (text === "cube")    { text = Ext.ux.Translator.translate('app_tasks_component'); cls = "cube"; };
          if (text === "star")    { text = Ext.ux.Translator.translate('app_tasks_feature'); cls = "star"; };
          if (text === "spider")  { text = Ext.ux.Translator.translate('app_tasks_critical_bug'); cls = "spider"; };
          if (text === "bug")     { text = Ext.ux.Translator.translate('app_tasks_bug'); cls = "bug"; };
          if (text === "brush")   { text = Ext.ux.Translator.translate('app_tasks_chore'); cls = "brush"; };
          if (text === "console") { text = Ext.ux.Translator.translate('app_tasks_admin'); cls = "console"; };
          if (text === "tool")    { text = Ext.ux.Translator.translate('app_tasks_tweak'); cls = "tool"; };
          if (text === "colors")  { text = Ext.ux.Translator.translate('app_tasks_design'); cls = "colors"; };
        };
        
        //if the filter is state change the text into a translation
        if (this.name == "state") {
          if (text == "pending") text = Ext.ux.Translator.translate('app_tasks_started');
          else if (text == "new") text = Ext.ux.Translator.translate('app_tasks_pending');
          else text = Ext.ux.Translator.translate('app_tasks_' + text);
        };
        
        if (this.displayField == "recipient_nickname") {
          if (!valueField) {
            id   = "all";
            text = Ext.ux.Translator.translate('everyone');
          };
        };
        
        //check if a translation exists
        if (Ext.ux.Translator.translate("app_tasks_" + text, null, null, true)) text = Ext.ux.Translator.translate("app_tasks_" + text);
        
        //split up the name
        var name = (this.name.split('.').length > 1) ? this.name.split('.')[0] : this.name;
        
        //get the iconCls
        var iconCls = (name == "state") ? name + " " + id : name + " " + text + " " + this.iconCls;
  			if (cls != "") iconCls = name + " " + cls + " " + this.iconCls;
        
        this.addOption(id, text, iconCls);
      };
    // }, this);
    // this.fireEvent('refreshed', this);
    
    if (this.valueField == "priority" || this.valueField == "effort") this.options.sort(this.sortLowMediumHigh);
    else this.options.sort(this.sortAlphabetically);
  },
  
  /**
	 * 
	 */
	sortLowMediumHigh: function(a, b) {
	  if (!a || !a.text || !b || !b.text) return 0;
	  
		var nameA = a.text.toLowerCase();
		var nameB = b.text.toLowerCase();
		
		if (nameA == "low" && nameB == "medium") return -1;
		if (nameA == "low" && nameB == "high") return -1;
		if (nameA == "medium" && nameB == "low") return 1;
		if (nameA == "medium" && nameB == "high") return -1;
		if (nameA == "high" && nameB == "low") return 1;
		if (nameA == "high" && nameB == "medium") return 1;
		
		return 0;
	},
  
  addOption: function(id, text, iconCls) {
    //check if the hashtag has already been added
    if (this.optionsPlain.indexOf(id) != -1) return;
    
    //push only the match to an array so we can check again above
    this.optionsPlain.push(id);
    
    //push the proper match..
    this.options.push({
      id:      id,
      text:    text || id,
      iconCls: iconCls
    });
  }
});

Ext.ux.FilterManager.reg('store-helper', Ext.ux.Filter.StoreHelper);

/**
 * @class Ext.ux.Filter.Tags
 * @extends Ext.ux.Filter
 * A configurable filter for filtering tags on a given view.
 * Requests tag data for the current object from the server
 */
Ext.ux.Filter.Tags = Ext.extend(Ext.ux.Filter, {
  
  constructor: function() {
    this.store = new Ext.data.JsonStore({
      root:   'tags',
      url:    '/tags/for',
      fields: ['name', 'id']
    });
    
    Ext.ux.Filter.Tags.superclass.constructor.apply(this, arguments);
    
    Ext.applyIf(this, {
      title:   Ext.ux.Translator.translate('tags'),
      name:    'tags',
      iconCls: 'tag'
    });
  },
  
  /**
   * Overrides the default eventReceived.  If the first argument is a model instance the
   * store's url is updated to match the URL for this particular instance, and the store
   * is reloaded.  If not a model instance, this just refreshes.
   * @param {Ext.data.Store} store The store full of records
   */
  eventReceived: function(store) {
    //TODO: this is not a good way of doing this test, but instanceof won't work until
    //MVC 0.6, at which point we can use instance instanceof Ext.data.Record
    if (store && store.data && store.data.items && store.data.items[0] && typeof store.data.items[0].newRecord != 'undefined') {
			var	instance = store.data.items[0];

			this.store.proxy.conn.url = this.urlFor(instance);

			this.store.reload({
				scope:    this,
				callback: this.refresh
			});

			//keep a reference to the currently loaded instance so as not to refresh too often
			this.currentModelInstance = instance;
		} else if (store && typeof store.newRecord != 'undefined') {
			this.store.proxy.conn.url = this.urlFor(store);
       
			this.store.reload({
				scope:    this,
				callback: this.refresh
			});

			//keep a reference to the currently loaded instance so as not to refresh too often
			this.currentModelInstance = store;
    } else {
      this.refresh();
    };
  },

  refresh: function() {
    this.options = [];
    
    var tags = [];
		
		if (!this.store) return;
		
    //build a unique array of tags
    this.store.each(function(tag) {
      if (tags.indexOf(tag.data.name) == -1) tags.push(tag.data.name);
    });
		
    Ext.each(tags, function(tag) {
      this.options.push({
        id:      tag,
        text:    tag,
        iconCls: this.iconCls
      });
    }, this);
		
		this.options.sort(this.sortAlphabetically);
    
    this.fireEvent('refreshed', this);
  },
  
  /**
   * Builds the url to retrieve tags from for a given model instance
   * @param {ExtMVC.Model.Base} instance The model instance
   * @return {String} The url for this instance
   */
  urlFor: function(instance) {
		var className = instance.className;
    var id = (className == "Album") ? instance.get('id') : Kohive.OS.getCurrentHive().data.hive.id;
		
		if (className == "File") { className = "FileCabinet"; };
    
    return String.format("/tags/for/{0}/{1}", className, id);
  }
});

Ext.ux.FilterManager.reg('tags', Ext.ux.Filter.Tags);

Ext.ns('Ext.ux.SortableDataViewPanel');

/**
 * @class Ext.ux.SortableDataViewPanel
 * @extends Ext.Panel
 * Provides a sortable wrapper around a DataView, with optional tabs
 * @cfg {Array} mainPanelBefore An array of components to insert before the DataView
 * @cfg {Array} mainPanelAfter An array of components to insert after the DataView
 */
Ext.ux.SortableDataViewPanel = function(config) {
  var config = config || {};
  config.dataViewConfig = config.dataViewConfig    || {};
  
  /**
   * Apply defaults for the internal DataView
   */
  Ext.applyIf(config.dataViewConfig, {
    multiSelect:  true,
    simpleSelect: true,
    autoHeight:   true,
    overClass:    'x-view-over'
  });
  
  /**
   * Apply defaults for before and after mainpanel content.  Allows you to insert components before
   * and after the dataview
   */
  Ext.applyIf(config, {
    mainPanelBefore: [],
    mainPanelAfter:  []
  });
  
  /**
   * @property tabs
   * @type Ext.ux.SortableDataViewPanel.TabPanel
   * Reference to the Ext.ux.SortableDataViewPanel.TabPanel instance used internally by the component
   */
  if (config.tabPanelConfig) {
    this.tabs = new Ext.ux.SortableDataViewPanel.TabPanel(config.tabPanelConfig);
  };
  
  /**
   * @property dataview
   * @type Ext.DataView
   * Reference to Ext.DataView instance used internally by the component
   */
  this.dataview = new Ext.DataView(config.dataViewConfig);
  
  /**
   * @property columns
   * @type Ext.ux.SortableDataViewPanel.ColumnPanel
   * Reference to the Ext.ux.SortableDataViewPanel.ColumnPanel instance used internally by the component
   */
  if (config.columnPanelConfig) {
    this.columns = new Ext.ux.SortableDataViewPanel.ColumnPanel(config.columnPanelConfig);
  };
  
  if (this.dataview.store && config.columnPanelConfig) {
    this.columns.on('changesortorder', this.dataview.store.sort, this.dataview.store);
  };
  
  /**
   * @property headerPanelItems
   * @type Array
   * An array of items which will be put into the header panel.
   */
  this.headerPanelItems = [];
  
  // Check if the tab panel is enabled
  if (config.tabPanelConfig) { this.headerPanelItems.push(this.tabs); };
  if (config.columnPanelConfig) { this.headerPanelItems.push(this.columns); };
  
  /**
   * @property headerPanel
   * @type Ext.Panel
   * Panel containing the tabs and columns objects
   */
  this.headerPanel = new Ext.Panel({
    region:     'north',
    autoHeight: true,
    border:     false,
    cls:        'x-sortable-data-view-header',
    items:      this.headerPanelItems
  });
  
  /**
   * @property mainPanel
   * @type Ext.Panel
   * Panel containing the DataView and any other components which are added
   */
  this.mainPanel = new Ext.Panel({
    region:     'center',
    items:      config.mainPanelBefore.concat([this.dataview]).concat(config.mainPanelAfter),
    border:     false,
    autoScroll: true,
    cls:        'x-sortable-data-view-body'
  });
 
  Ext.applyIf(config, {
    layout: 'border',
    border: false,
    items: [
      this.headerPanel,
      this.mainPanel
    ]
  });
 
  Ext.ux.SortableDataViewPanel.superclass.constructor.call(this, config);
};
Ext.extend(Ext.ux.SortableDataViewPanel, Ext.Panel);

Ext.reg('sortable_data_view_panel', Ext.ux.SortableDataViewPanel);

/**
 * @class Ext.ux.SortableDataViewPanel.ColumnHeader
 * @extends Ext.Component
 * Represents an individual column in a ColumnPanel, maintaining sort state
 */
Ext.ux.SortableDataViewPanel.ColumnHeader = Ext.extend(Ext.Component, {
  
  /**
   * @property ASCCls
   * @type String
   * The class which is used when this column header is sorted ascending
   */
  ASCCls: 'x-asc',
  
  /**
   * @property DESCCls
   * @type String
   * The class which is used when this column header is sorted descending
   */
  DESCCls: 'x-desc',
  
  /**
   * Creates the ColumnHeader object and applies defaults
   * @param {Object} config The Tab config (should contain at least a field property)
   */
  constructor: function(config) {
    var config = config || {};
    
    this.field = config.field || "";
    
    Ext.applyIf(config, {
      text: this.field.capitalize()
    });
    
    Ext.ux.SortableDataViewPanel.ColumnHeader.superclass.constructor.apply(this, arguments);
  },
  
  /**
   * Add events and set up instance variables
   */
  initComponent: function() {
    Ext.ux.SortableDataViewPanel.ColumnHeader.superclass.initComponent.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event changesortorder
       * Fires when the column header is clicked, passing a reference to the column header itself and the new sort direction
       * @param {Ext.ux.SortableDataViewPanel.ColumnHeader} this The ColumnHeader object
       * @param {String/Null} sortDirection The direction this column header is sorted in (ASC, DESC or null)
       */
      'changesortorder'
    );
    
    this.sortDirection = null;
  },
  
  /**
   * Creates the HTML required for this component
   */
  onRender: function(ct, position) {
    this.el = ct.createChild({
      tag: 'li',
      cls: 'kohive-column-header',
      children: [
        {
          tag:  'span',
          html: this.initialConfig.text
        }
      ]
    });
    
    this.titleEl = this.el.child('span');
    
    Ext.ux.SortableDataViewPanel.ColumnHeader.superclass.onRender.apply(this, arguments);
    
    this.el.unselectable();
    this.el.on('click', this.toggleSortDirection, this);
  },
  
  /**
   * Changes the title of this column header
   * @param {String} title The new title for the Column Header
   */
  setTitle: function(title) {
    this.titleEl.update(title);
  },
  
  /**
   * Toggles the internal sort order representation, updates the sort image
   * @param {String} direction ASC or DESC
   */
  setSortDirection: function(direction) {
    if (direction == 'ASC' || direction == 'DESC') {
      direction == 'ASC' ? this.sortAscending() : this.sortDescending();
    }
  },
  
  /**
   * Sets this column header's sort direction to null, removes sort image
   */
  removeSortDirection: function() {
    this.sortDirection = null;
    this.updateElCls();
  },
  
  /**
   * Toggles the sort direction between ASC and DESC, depending on its current value.
   * If there is no current sort direction, ASC is used
   */
  toggleSortDirection: function() {
    this.sortDirection == 'ASC' ? this.sortDescending() : this.sortAscending();
  },
  
  /**
   * Updates this.sortDirection, fires a changesortorder event and updates the sort image
   */
  sortAscending: function() {
    this.sortDirection = 'ASC';
    this.updateElCls(this.ASCCls);
    this.fireEvent('changesortorder', this, this.sortDirection);
  },
  
  /**
   * Updates this.sortDirection, fires a changesortorder event and updates the sort image
   */
  sortDescending: function() {
    this.sortDirection = 'DESC';
    this.updateElCls(this.DESCCls);
    this.fireEvent('changesortorder', this, this.sortDirection);
  },
  
  /**
   * Private method. Updates the column with the correct class
   * @param {String} sortCls The new sort class for the element
   */
  updateElCls: function(sortCls) {
    // Remove old classes
    this.el.removeClass(this.DESCCls);
    this.el.removeClass(this.ASCCls);
    
    // Add the new class if it isn't blank
    if(sortCls){
      this.el.addClass(sortCls);
    }
  }
});

Ext.reg('sortable_data_view_column_header', Ext.ux.SortableDataViewPanel.ColumnHeader);

/**
 * @class Ext.ux.SortableDataViewPanel.ColumnPanel
 * @extends Ext.Container
 * Provides clickable columns which fire events to re-order a DataView
 */
Ext.ux.SortableDataViewPanel.ColumnPanel = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    defaults: {
      xtype: 'sortable_data_view_column_header'
    }
  });
  
  Ext.ux.SortableDataViewPanel.ColumnPanel.superclass.constructor.call(this, config);

  this.addEvents(
    /**
     * @event changesortorder
     * Fires when the column header is clicked, passing a reference to the column header itself and the new sort direction
     * @param {String} field The field to sort on
     * @param {String} sortDirection The direction this column header is sorted in (ASC, DESC or null)
     */
    'changesortorder'
  );
};
Ext.extend(Ext.ux.SortableDataViewPanel.ColumnPanel, Ext.Container, {
  
  /**
   * Renders the HTML for this component
   * @param {Ext.Container} ct The container to render this component to
   * @param {Number} position The position within the parent container to render this component to
   */
  onRender: function(ct, position) {
    this.el = ct.createChild({
      cls: 'kohive-column-panel',
      children: [{tag: 'ul'}]
    });
    
    this.layoutTarget = this.el.child('ul');

    Ext.ux.SortableDataViewPanel.ColumnPanel.superclass.onRender.apply(this, arguments);
    
    this.items.each(function(item) {
      item.on('changesortorder', this.relayChangeSortOrderEvent, this);
    }, this);
  },
  
  /**
   * Returns the HTML element to render child components to
   * @return {Ext.Element} The element child components will be rendered to
   */
  getLayoutTarget: function() {
    return this.layoutTarget;
  },
  
  /**
   * Relays the changesortorder event to the containing object, unsorts all other headers
   * @param {Ext.ux.SortableDataViewPanel.ColumnHeader} columnHeader The column header which has been clicked
   */
  relayChangeSortOrderEvent: function(columnHeader, direction) {
    this.removeSortOrders(columnHeader);
    this.fireEvent('changesortorder', columnHeader.field, direction);
  },
  
  /**
   * Removes the sort order from all column headers.  Specify a column header argument to not unset its sort order
   * @param {Ext.ux.SortableDataViewPanel.ColumnHeader} exceptColumnHeader An optional column header to not remove sort order from
   */
  removeSortOrders: function(exceptColumnHeader) {
    this.items.each(function(item) {
      if (item != exceptColumnHeader) {
        item.removeSortDirection();
      }
    }, this);
  }
});

Ext.reg('sortable_data_view_column_panel', Ext.ux.SortableDataViewPanel.ColumnPanel);

/**
 * @class Ext.ux.SortableDataViewPanel.Tab
 * @extends Ext.Component
 * Represents a single Tab inside a Ext.ux.SortableDataViewPanel.TabPanel
 */
Ext.ux.SortableDataViewPanel.Tab = Ext.extend(Ext.Component, {
  
  /**
   * @property activeTabClass
   * @type String
   * The CSS class to add to this Tab when it is activated
   */
  activeTabClass: 'kohive-tab-active',
  
  initComponent: function() {
    this.addEvents(
      /**
       * @event tabclick
       * Fires when the tab is clicked
       */
      'tabclick'
    );
    
    Ext.ux.SortableDataViewPanel.Tab.superclass.initComponent.apply(this, arguments);
  },
  
  /**
   * Creates the HTML elements for a single Tab
   * @param {Ext.Container} ct The container to add this Tab to
   */
  onRender: function(ct, position) {
    this.el = ct.createChild({
      tag: 'li',
      cls: 'kohive-tab-wrapper',
      children: [
        { tag: 'span', cls: 'kohive-tab-left' },
        { tag: 'span', cls: 'kohive-tab-center', html: this.initialConfig.text },
        { tag: 'span', cls: 'kohive-tab-right'}
      ]
    });
    
    Ext.ux.SortableDataViewPanel.Tab.superclass.onRender.apply(this, arguments);
    
    this.el.on('click', function() { this.fireEvent('clicker'); }, this);
  },
  
  activate: function() {
    this.el.addClass(this.activeTabClass);
    ////console.log('activating');
  },
  
  deactivate: function() {
    this.el.removeClass(this.activeTabClass);
    ////console.log('deactivating');
  }
});

Ext.reg('sortable_data_view_tab', Ext.ux.SortableDataViewPanel.Tab);

/**
 * @class Ext.ux.SortableDataViewPanel.TabPanel
 * @extends Ext.Container
 * Manages addition and removal of tabs
 */
Ext.ux.SortableDataViewPanel.TabPanel = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    defaults: {
      xtype: 'kohive_sortable_data_view_tab'
    }
  });
  
  this.addEvents(
    /**
     * @event beforetabchange
     * Fires before the clicked tab is set as the current tab.  Return false from any handler to cancel
     * @param {Ext.ux.SortableDataViewPanel.Tab} tab The tab which is about to be set active
     * @param {Ext.ux.SortableDataViewPanel.Tab} currentTab The tab which is currently active
     */
    'beforetabchange',
    
    /**
     * @event tabchange
     * Fires after the active tab has been set to a new tab
     * @param {Ext.ux.SortableDataViewPanel.Tab} tab The newly activated tab
     * @param {Ext.ux.SortableDataViewPanel.Tab} oldTab The tab which was previously active
     */
    'tabchange'
  );
 
  Ext.ux.SortableDataViewPanel.TabPanel.superclass.constructor.call(this, config);
};
Ext.extend(Ext.ux.SortableDataViewPanel.TabPanel, Ext.Container, {
  
  /**
   * @property activeTab
   * @type Ext.ux.SortableDataViewPanel.Tab
   * A reference to the currently active tab
   */
  activeTab: null,
  
  /**
   * Sets the active tab to the one specified
   * @param {Ext.ux.SortableDataViewPanel.Tab} tab The tab to activate
   */
  setActiveTab: function(tab) {
    var currentlyActiveTab = this.activeTab;
    
    if (this.fireEvent('beforetabchange', tab, currentlyActiveTab)) {
      this.items.each(function(item) {item.deactivate();});
      tab.activate();
      
      this.fireEvent('tabchange', tab, currentlyActiveTab);
    }
  },
  
  /**
   * Renders the HTML for this component
   * @param {Ext.Container} ct The container to render this component to
   * @param {Number} position The position within the parent container to render this component to
   */
  onRender: function(ct, position) {
    this.el = ct.createChild({
      cls: 'kohive-tab-panel',
      children: [{tag: 'ul'}]
    });
    
    this.layoutTarget = this.el.child('ul');

    Ext.ux.SortableDataViewPanel.TabPanel.superclass.onRender.apply(this, arguments);
    
    this.items.each(function(tab) {
      tab.on('tabclick', this.setActiveTab, this);
    });
  },
  
  /**
   * Returns the HTML element to render child components to
   * @return {Ext.Element} The element child components will be rendered to
   */
  getLayoutTarget: function() {
    return this.layoutTarget;
  }
});

Ext.reg('sortable_data_view_tab_panel', Ext.ux.SortableDataViewPanel.TabPanel);

Ext.namespace('Ext.ux.form');
/**
 * <p>SuperBoxSelect is an extension of the ComboBox component that displays selected items as labelled boxes within the form field. As seen on facebook, hotmail and other sites.</p>
 * <p>The SuperBoxSelect component was inspired by the BoxSelect component found here: http://efattal.fr/en/extjs/extuxboxselect/</p>
 * 
 * @author <a href="mailto:dan.humphrey@technomedia.co.uk">Dan Humphrey</a>
 * @class Ext.ux.form.SuperBoxSelect
 * @extends Ext.form.ComboBox
 * @constructor
 * @component
 * @version 1.0b
 * @license TBA
 * 
 */
Ext.ux.form.SuperBoxSelect = function(config) {
    Ext.ux.form.SuperBoxSelect.superclass.constructor.call(this,config);
    this.addEvents(
        /**
         * Fires before an item is added to the component via user interaction. Return false from the callback function to prevent the item from being added.
         * @event beforeadditem
         * @memberOf Ext.ux.form.SuperBoxSelect
         * @param {SuperBoxSelect} this
         * @param {Mixed} value The value of the item to be added
         */
        'beforeadditem',

        /**
         * Fires after a new item is added to the component.
         * @event additem
         * @memberOf Ext.ux.form.SuperBoxSelect
         * @param {SuperBoxSelect} this
         * @param {Mixed} value The value of the item which was added
         * @param {Record} record The store record which was added
         */
        'additem',

        /**
         * Fires when the allowAddNewData config is set to true, and a user attempts to add an item that is not in the data store.
         * @event newitem
         * @memberOf Ext.ux.form.SuperBoxSelect
         * @param {SuperBoxSelect} this
         * @param {Mixed} value The new item's value
         */
        'newitem',

        /**
         * Fires when an item's remove button is clicked. Return false from the callback function to prevent the item from being removed.
         * @event beforeremoveitem
         * @memberOf Ext.ux.form.SuperBoxSelect
         * @param {SuperBoxSelect} this
         * @param {Mixed} value The value of the item to be removed
         */
        'beforeremoveitem',

        /**
         * Fires after an item has been removed.
         * @event removeitem
         * @memberOf Ext.ux.form.SuperBoxSelect
         * @param {SuperBoxSelect} this
         * @param {Mixed} value The value of the item which was removed
         * @param {Record} record The store record which was removed
         */
        'removeitem',
        /**
         * Fires after the component values have been cleared.
         * @event clear
         * @memberOf Ext.ux.form.SuperBoxSelect
         * @param {SuperBoxSelect} this
         */
        'clear'
    );
    
};

/**
 * @private hide from doc gen
 */
Ext.ux.form.SuperBoxSelect = Ext.extend(Ext.ux.form.SuperBoxSelect,Ext.form.ComboBox,{
    /**
     * @cfg {Boolean} allowAddNewData When set to true, allows items to be added (via the setValueEx and addItem methods) that do not already exist in the data store. Defaults to false.
     */
    allowAddNewData: false,

    /**
     * @cfg {Boolean} backspaceDeletesLastItem When set to false, the BACKSPACE key will focus the last selected item. When set to true, the last item will be immediately deleted. Defaults to true.
     */
    backspaceDeletesLastItem: true,

    /**
     * @cfg {String} classField The underlying data field that will be used to supply an additional class to each item.
     */
    classField: null,

    /**
     * @cfg {String} clearBtnCls An additional class to add to the in-field clear button.
     */
    clearBtnCls: '',

    /**
     * @cfg {String/XTemplate} displayFieldTpl A template for rendering the displayField in each selected item. Defaults to null.
     */
    displayFieldTpl: null,

    /**
     * @cfg {String} extraItemCls An additional css class to apply to each item.
     */
    extraItemCls: '',

    /**
     * @cfg {String/Object/Function} extraItemStyle Additional css style(s) to apply to each item. Should be a valid argument to Ext.Element.applyStyles.
     */
    extraItemStyle: '',

    /**
     * @cfg {String} expandBtnCls An additional class to add to the in-field expand button.
     */
    expandBtnCls: '',

    /**
     * @cfg {Boolean} fixFocusOnTabSelect When set to true, the component will not lose focus when a list item is selected with the TAB key. Defaults to true.
     */
    fixFocusOnTabSelect: true,
    
     /**
     * @cfg {Boolean} forceFormValue When set to true, the component will always return a value to the parent form getValues method, and when the parent form is submitted manually. Defaults to false, meaning the component will only be included in the parent form submission (or getValues) if at least 1 item has been selected.  
     */
    forceFormValue: true,
    
    /**
     * @cfg {Boolean} navigateItemsWithTab When set to true the tab key will navigate between selected items. Defaults to true.
     */
    navigateItemsWithTab: true,

    /**
     * @cfg {Boolean} pinList When set to true the select list will be pinned to allow for multiple selections. Defaults to true.
     */
    pinList: true,

    /**
     * @cfg {Boolean} preventDuplicates When set to true unique item values will be enforced. Defaults to true.
     */
    preventDuplicates: true,
    
    /**
     * @cfg {String} queryValuesDelimiter Used to delimit multiple values queried from the server when mode is remote.
     */
    queryValuesDelimiter: '|',
    
    /**
     * @cfg {String} queryValuesIndicator A request variable that is sent to the server (as true) to indicate that we are querying values rather than display data (as used in autocomplete) when mode is remote.
     */
    queryValuesInidicator: 'valuesqry',

    /**
     * @cfg {Boolean} removeValuesFromStore When set to true, selected records will be removed from the store. Defaults to true.
     */
    removeValuesFromStore: true,

    /**
     * @cfg {String} renderFieldBtns When set to true, will render in-field buttons for clearing the component, and displaying the list for selection. Defaults to true.
     */
    renderFieldBtns: true,

    /**
     * @cfg {Boolean} stackItems When set to true, the items will be stacked 1 per line. Defaults to false which displays the items inline.
     */
    stackItems: false,

    /**
     * @cfg {String} styleField The underlying data field that will be used to supply additional css styles to each item.
     */
    styleField : null,
    
     /**
     * @cfg {Boolean} supressClearValueRemoveEvents When true, the removeitem event will not be fired for each item when the clearValue method is called, or when the clear button is used. Defaults to false.
     */
    supressClearValueRemoveEvents : false,
    
    /**
     * @cfg {String/Boolean} validationEvent The event that should initiate field validation. Set to false to disable automatic validation (defaults to 'blur').
     */
	validationEvent : 'blur',
	
    /**
     * @cfg {String} valueDelimiter The delimiter to use when joining and splitting value arrays and strings.
     */
    valueDelimiter: ',',
    initComponent:function() {
       Ext.apply(this, {
            items           : new Ext.util.MixedCollection(false),
            usedRecords     : new Ext.util.MixedCollection(false),
            addedRecords	: [],
            remoteLookup	: [],
            hideTrigger     : true,
            grow            : false,
            resizable       : false,
            multiSelectMode : false,
            preRenderValue  : null
        });
        
        if(this.transform){
            this.doTransform();
        }
    
        if(this.forceFormValue){
        	this.items.on({
        	   add: this.manageNameAttribute,
        	   remove: this.manageNameAttribute,
        	   clear: this.manageNameAttribute,
        	   scope: this
        	});
        }
        
        Ext.ux.form.SuperBoxSelect.superclass.initComponent.call(this);
        if(this.mode === 'remote' && this.store){
        	this.store.on('load', this.onStoreLoad, this);
        }
    },
    onRender:function(ct, position) {
        Ext.ux.form.SuperBoxSelect.superclass.onRender.call(this, ct, position);
        
        this.el.dom.removeAttribute('name');
        
        
        var extraClass = (this.stackItems === true) ? 'x-superboxselect-stacked' : '';
        if(this.renderFieldBtns){
            extraClass += ' x-superboxselect-display-btns';
        }
        this.el.removeClass('x-form-text').addClass('x-superboxselect-input-field');
        
        this.wrapEl = this.el.wrap({
            tag : 'ul'
        });
        
        this.outerWrapEl = this.wrapEl.wrap({
            tag : 'div',
            cls: 'x-form-text x-superboxselect ' + extraClass
        });
       
        this.inputEl = this.el.wrap({
            tag : 'li',
            cls : 'x-superboxselect-input'
        });
        
        if(this.renderFieldBtns){
            this.setupFieldButtons().manageClearBtn();
        }
        
        this.setupFormInterception();
        
        if(this.preRenderValue){
            this.setValue(this.preRenderValue);
            this.preRenderValue = null;
        };
    },
    onStoreLoad : function(store, records, options){
    	//accomodating for bug in Ext 3.0.0 where options.params are empty
    	var q = options.params[this.queryParam] || store.baseParams[this.queryParam] || "",
    		isValuesQuery = options.params[this.queryValuesInidicator] || store.baseParams[this.queryValuesInidicator];
    	
    	if(this.removeValuesFromStore){
    		this.store.each(function(record) {
				if(this.usedRecords.containsKey(record.get(this.valueField))){
					this.store.remove(record);
				}
			}, this);
    	}
    	//queried values
    	if(isValuesQuery){
    		var params = q.split(this.queryValuesDelimiter);
    		Ext.each(params,function(p){
    			this.remoteLookup.remove(p);
    			var rec = this.findRecord(this.valueField,p);
    			if(rec){
    				this.addRecord(rec);
    			}
    		},this);
    		
    		if(this.setOriginal){
    			this.setOriginal = false;
    			this.originalValue = this.getValue();
    		}
    	}

    	//queried display (autocomplete) & addItem
    	if(q !== '' && this.allowAddNewData){
    		Ext.each(this.remoteLookup,function(r){
    			if(typeof r == "object" && r[this.displayField] == q){
    				this.remoteLookup.remove(r);
					if(records.length && records[0].get(this.displayField) === q) {
						this.addRecord(records[0]);
						return;
					}
					var rec = this.createRecord(r);
					this.store.add(rec);
		        	this.addRecord(rec);
		        	this.addedRecords.push(rec); //keep track of records added to store
		        	(function(){
		        		if(this.isExpanded()){
			        		this.collapse();
		        		}
		        	}).defer(10,this);
		        	return;
    			}
    		},this);
    	}
    	
    	var toAdd = [];
    	if(q === ''){
	    	Ext.each(this.addedRecords,function(rec){
	    		if(this.preventDuplicates && this.usedRecords.containsKey(rec.get(this.valueField))){
					return;	    			
	    		}
	    		toAdd.push(rec);
	    		
	    	},this);
	    	
    	}else{
    		var re = new RegExp(Ext.escapeRe(q) + '.*','i');
    		Ext.each(this.addedRecords,function(rec){
	    		if(this.preventDuplicates && this.usedRecords.containsKey(rec.get(this.valueField))){
					return;	    			
	    		}
	    		if(re.test(rec.get(this.displayField))){
	    			toAdd.push(rec);
	    		}
	    	},this);
	    }
    	this.store.add(toAdd);
    	this.store.sort(this.displayField, 'ASC');
    	
		if(this.store.getCount() === 0 && this.isExpanded()){
			this.collapse();
		}
		
	},
    doTransform : function() {
    	var s = Ext.getDom(this.transform), transformValues = [];
            if(!this.store){
                this.mode = 'local';
                var d = [], opts = s.options;
                for(var i = 0, len = opts.length;i < len; i++){
                    var o = opts[i],
                        value = (Ext.isIE && !Ext.isIE8 ? o.getAttributeNode('value').specified : o.hasAttribute('value')) ? o.value : o.text,
                        cls = (Ext.isIE && !Ext.isIE8 ? o.getAttributeNode('class').specified : o.hasAttribute('class')) ? o.className : '',
                        style = (Ext.isIE && !Ext.isIE8 ? o.getAttributeNode('style').specified : o.hasAttribute('style')) ? o.style : '';
                    if(o.selected) {
                        transformValues.push(value);
                    }
                    d.push([value, o.text, cls, style.cssText]);
                }
                this.store = new Ext.data.SimpleStore({
                    'id': 0,
                    fields: ['value', 'text', 'cls', 'style'],
                    data : d
                });
                Ext.apply(this,{
                    valueField: 'value',
                    displayField: 'text',
                    classField: 'cls',
                    styleField: 'style'
                });
            }
           
            if(transformValues.length){
                this.value = transformValues.join(',');
            }
    },
    setupFieldButtons : function(){
        this.buttonWrap = this.outerWrapEl.createChild({
            cls: 'x-superboxselect-btns'
        });
        
        this.buttonClear = this.buttonWrap.createChild({
            tag:'div',
            cls: 'x-superboxselect-btn-clear ' + this.clearBtnCls
        });
        
        this.buttonExpand = this.buttonWrap.createChild({
            tag:'div',
            cls: 'x-superboxselect-btn-expand ' + this.expandBtnCls
        });
        
        this.initButtonEvents();
        
        return this;
    },
    initButtonEvents : function() {
        this.buttonClear.addClassOnOver('x-superboxselect-btn-over').on('click', function(e) {
            e.stopEvent();
            if (this.disabled) {
                return;
            }
            this.clearValue();
            this.el.focus();
        }, this);

        this.buttonExpand.addClassOnOver('x-superboxselect-btn-over').on('click', function(e) {
            e.stopEvent();
            if (this.disabled) {
                return;
            }
            if (this.isExpanded()) {
                this.multiSelectMode = false;
            } else if (this.pinList) {
                this.multiSelectMode = true;
            }
            this.onTriggerClick();
        }, this);
    },
    removeButtonEvents : function() {
        this.buttonClear.removeAllListeners();
        this.buttonExpand.removeAllListeners();
        return this;
    },
    clearCurrentFocus : function(){
        if(this.currentFocus){
            this.currentFocus.onLnkBlur();
            this.currentFocus = null;
        }  
        return this;        
    },
    initEvents : function() {
        var el = this.el;

        el.on({
            click   : this.onClick,
            focus   : this.clearCurrentFocus,
            blur    : this.onBlur,

            keydown : this.onKeyDownHandler,
            keyup   : this.onKeyUpBuffered,

            scope   : this
        });

        this.on({
            collapse: this.onCollapse,
            expand: this.clearCurrentFocus,
            scope: this
        });

        this.wrapEl.on('click', this.onWrapClick, this);
        this.outerWrapEl.on('click', this.onWrapClick, this);
        
        this.inputEl.focus = function() {
            el.focus();
        };

        Ext.ux.form.SuperBoxSelect.superclass.initEvents.call(this);

        Ext.apply(this.keyNav, {
            tab: function(e) {
                if (this.fixFocusOnTabSelect && this.isExpanded()) {
                    e.stopEvent();
                    el.blur();
                    this.onViewClick(false);
                    this.focus(false, 10);
                    return true;
                }

                this.onViewClick(false);
                if (el.dom.value !== '') {
                    this.setRawValue('');
                }

                return true;
            },

            down: function(e) {
                if (!this.isExpanded() && !this.currentFocus) {
                    this.onTriggerClick();
                } else {
                    this.inKeyMode = true;
                    this.selectNext();
                }
            },

            enter: function(){}
        });
    },

    onClick: function() {
        this.clearCurrentFocus();
        this.collapse();
        this.autoSize();
    },

    beforeBlur: Ext.form.ComboBox.superclass.beforeBlur,

    onFocus: function() {
        this.outerWrapEl.addClass(this.focusClass);

        Ext.ux.form.SuperBoxSelect.superclass.onFocus.call(this);
    },

    onBlur: function() {
        this.outerWrapEl.removeClass(this.focusClass);

        this.clearCurrentFocus();
        
        if (this.el.dom.value !== '') {
            var obj = {};
            obj[this.valueField] = this.el.dom.value;
            this.addItem(obj);
            this.applyEmptyText();
            this.autoSize();
        }

        Ext.ux.form.SuperBoxSelect.superclass.onBlur.call(this);
    },

    onCollapse: function() {
    	this.view.clearSelections();
        this.multiSelectMode = false;
    },

    onWrapClick: function(e) {
        e.stopEvent();
        this.collapse();
        this.el.focus();
        this.clearCurrentFocus();
    },
    markInvalid : function(msg) {
        var elp, t;

        if (!this.rendered || this.preventMark ) {
            return;
        }
        this.outerWrapEl.addClass(this.invalidClass);
        msg = msg || this.invalidText;

        switch (this.msgTarget) {
            case 'qtip':
                Ext.apply(this.el.dom, {
                    qtip    : msg,
                    qclass  : 'x-form-invalid-tip'
                });
                Ext.apply(this.wrapEl.dom, {
                    qtip    : msg,
                    qclass  : 'x-form-invalid-tip'
                });
                if (Ext.QuickTips) { // fix for floating editors interacting with DND
                    Ext.QuickTips.enable();
                }
                break;
            case 'title':
                this.el.dom.title = msg;
                this.wrapEl.dom.title = msg;
                this.outerWrapEl.dom.title = msg;
                break;
            case 'under':
                if (!this.errorEl) {
                    elp = this.getErrorCt();
                    if (!elp) { // field has no container el
                        this.el.dom.title = msg;
                        break;
                    }
                    this.errorEl = elp.createChild({cls:'x-form-invalid-msg'});
                    this.errorEl.setWidth(elp.getWidth(true) - 20);
                }
                this.errorEl.update(msg);
                Ext.form.Field.msgFx[this.msgFx].show(this.errorEl, this);
                break;
            case 'side':
                if (!this.errorIcon) {
                    elp = this.getErrorCt();
                    if (!elp) { // field has no container el
                        this.el.dom.title = msg;
                        break;
                    }
                    this.errorIcon = elp.createChild({cls:'x-form-invalid-icon'});
                }
                this.alignErrorIcon();
                Ext.apply(this.errorIcon.dom, {
                    qtip    : msg,
                    qclass  : 'x-form-invalid-tip'
                });
                this.errorIcon.show();
                this.on('resize', this.alignErrorIcon, this);
                break;
            default:
                t = Ext.getDom(this.msgTarget);
                t.innerHTML = msg;
                t.style.display = this.msgDisplay;
                break;
        }
        this.fireEvent('invalid', this, msg);
    },
    clearInvalid : function(){
        if(!this.rendered || this.preventMark){ // not rendered
            return;
        }
        this.outerWrapEl.removeClass(this.invalidClass);
        switch(this.msgTarget){
            case 'qtip':
                this.el.dom.qtip = '';
                this.wrapEl.dom.qtip ='';
                break;
            case 'title':
                this.el.dom.title = '';
                this.wrapEl.dom.title = '';
                this.outerWrapEl.dom.title = '';
                break;
            case 'under':
                if(this.errorEl){
                    Ext.form.Field.msgFx[this.msgFx].hide(this.errorEl, this);
                }
                break;
            case 'side':
                if(this.errorIcon){
                    this.errorIcon.dom.qtip = '';
                    this.errorIcon.hide();
                    this.un('resize', this.alignErrorIcon, this);
                }
                break;
            default:
                var t = Ext.getDom(this.msgTarget);
                t.innerHTML = '';
                t.style.display = 'none';
                break;
        }
        this.fireEvent('valid', this);
    },
    alignErrorIcon : function(){
        if(this.wrap){
            this.errorIcon.alignTo(this.wrap, 'tl-tr', [Ext.isIE ? 5 : 2, 3]);
        }
    },
    expand : function(){
        if (this.isExpanded() || !this.hasFocus) {
            return;
        }
        this.list.alignTo(this.outerWrapEl, this.listAlign).show();
        this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac
        Ext.getDoc().on({
            mousewheel: this.collapseIf,
            mousedown: this.collapseIf,
            scope: this
        });
        this.fireEvent('expand', this);
    },
    restrictHeight : function(){
        var inner = this.innerList.dom,
            st = inner.scrollTop, 
            list = this.list;
        
        inner.style.height = '';
        
        var pad = list.getFrameWidth('tb')+(this.resizable?this.handleHeight:0)+this.assetHeight,
            h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight),
            ha = this.getPosition()[1]-Ext.getBody().getScroll().top,
            hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height,
            space = Math.max(ha, hb, this.minHeight || 0)-list.shadowOffset-pad-5;
        
        h = Math.min(h, space, this.maxHeight);
        this.innerList.setHeight(h);

        list.beginUpdate();
        list.setHeight(h+pad);
        list.alignTo(this.outerWrapEl, this.listAlign);
        list.endUpdate();
        
        if(this.multiSelectMode){
            inner.scrollTop = st;
        }
    },
    
    validateValue: function(val){
        if(this.items.getCount() === 0){
             if(this.allowBlank){
                 this.clearInvalid();
                 return true;
             }else{
                 this.markInvalid(this.blankText);
                 return false;
             }
        }
        
        this.clearInvalid();
        return true;
    },

    manageNameAttribute :  function(){
    	if(this.items.getCount() === 0){
    	   this.el.dom.setAttribute('name', this.name);
    	}else{
    		this.el.dom.removeAttribute('name');
    	}
    },
    setupFormInterception : function(){
        var form;
        this.findParentBy(function(p){ 
            if(p.getForm){
                form = p.getForm();
            }
        });
        if(form){
        	
        	var formGet = form.getValues;
            form.getValues = function(asString){
                this.el.dom.disabled = true;
                var oldVal = this.el.dom.value;
                this.setRawValue('');
                var vals = formGet.call(form);
                this.el.dom.disabled = false;
                this.setRawValue(oldVal);
                if(this.forceFormValue && this.items.getCount() === 0){
                	vals[this.name] = '';
                }
                return asString ? Ext.urlEncode(vals) : vals ;
            }.createDelegate(this);
        }
    },
    onResize : function(w, h, rw, rh) {
        var reduce = Ext.isIE6 ? 4 : Ext.isIE7 ? 1 : Ext.isIE8 ? 1 : 0;
        
        this._width = w;
        this.outerWrapEl.setWidth(w - reduce);
        if (this.renderFieldBtns) {
            reduce += (this.buttonWrap.getWidth() + 20);
            this.wrapEl.setWidth(w - reduce);
        }
        Ext.ux.form.SuperBoxSelect.superclass.onResize.call(this, w, h, rw, rh);
        this.autoSize();
    },
    onEnable: function(){
        Ext.ux.form.SuperBoxSelect.superclass.onEnable.call(this);
        this.items.each(function(item){
            item.enable();
        });
        if (this.renderFieldBtns) {
            this.initButtonEvents();
        }
    },
    onDisable: function(){
        Ext.ux.form.SuperBoxSelect.superclass.onDisable.call(this);
        this.items.each(function(item){
            item.disable();
        });
        if(this.renderFieldBtns){
            this.removeButtonEvents();
        }
    },
    /**
     * Clears all values from the component.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name clearValue
     * @param {Boolean} supressRemoveEvent [Optional] When true, the 'removeitem' event will not fire for each item that is removed.    
     */
    clearValue : function(supressRemoveEvent){
        Ext.ux.form.SuperBoxSelect.superclass.clearValue.call(this);
        this.preventMultipleRemoveEvents = supressRemoveEvent || this.supressClearValueRemoveEvents || false;
    	this.removeAllItems();
        this.fireEvent('clear',this);
        return this;
    },
    onKeyUp : function(e) {
        if (this.editable !== false && !e.isSpecialKey() && (!e.hasModifier() || e.shiftKey)) {
            this.lastKey = e.getKey();
            this.dqTask.delay(this.queryDelay);
        }        
    },
    onKeyDownHandler : function(e,t) {
        var toDestroy,nextFocus,idx;
        if ((e.getKey() === e.DELETE || e.getKey() === e.SPACE) && this.currentFocus){
            e.stopEvent();
            toDestroy = this.currentFocus;
            this.on('expand',function(){this.collapse();},this,{single: true});
            idx = this.items.indexOfKey(this.currentFocus.key);
            
            this.clearCurrentFocus();
            
            if(idx < (this.items.getCount() -1)){
                nextFocus = this.items.itemAt(idx+1);
            }
            
            toDestroy.preDestroy(true);
            if(nextFocus){
                (function(){
                    nextFocus.onLnkFocus();
                    this.currentFocus = nextFocus;
                }).defer(200,this);
            }
        
            return true;
        }
        
        var val = this.el.dom.value, it, ctrl = e.ctrlKey;
        if(e.getKey() === e.SPACE || e.getKey() === e.ENTER || e.getKey() === 188){
            e.stopEvent();
            if (val !== "") {
                if (ctrl || !this.isExpanded())  {  //ctrl+enter for new items
                	this.view.clearSelections();
                    this.collapse();
                    this.setRawValue('');
                    this.fireEvent('newitem', this, val);
                }
                else {
                	this.onViewClick();
                    //removed from 3.0.1
                    if(this.unsetDelayCheck){
                        this.delayedCheck = true;
                        this.unsetDelayCheck.defer(10, this);
                    }
                }
            }else{
                if(!this.isExpanded()){
                  this.fireEvent('enter-key', this);
                    return;
                }
                this.onViewClick();
                //removed from 3.0.1
                if(this.unsetDelayCheck){
                    this.delayedCheck = true;
                    this.unsetDelayCheck.defer(10, this);
                }
            }
            return true;
        } else if(e.getKey() === 9) {
          if (val !== "") {
            if (ctrl || !this.isExpanded())  {  //ctrl+enter for new items
            	this.view.clearSelections();
              this.collapse();
              this.setRawValue('');
              this.fireEvent('newitem', this, val);
            }
          };
        };
        
        if(val !== '') {
            this.autoSize();
            return;
        }
        
        //select first item
        if(e.getKey() === e.HOME){
            e.stopEvent();
            if(this.items.getCount() > 0){
                this.collapse();
                it = this.items.get(0);
                it.el.focus();
                
            }
            return true;
        }
        //backspace remove
        if(e.getKey() === e.BACKSPACE){
            e.stopEvent();
            if(this.currentFocus) {
                toDestroy = this.currentFocus;
                this.on('expand',function(){
                    this.collapse();
                },this,{single: true});
                
                idx = this.items.indexOfKey(toDestroy.key);
                
                this.clearCurrentFocus();
                if(idx < (this.items.getCount() -1)){
                    nextFocus = this.items.itemAt(idx+1);
                }
                
                toDestroy.preDestroy(true);
                
                if(nextFocus){
                    (function(){
                        nextFocus.onLnkFocus();
                        this.currentFocus = nextFocus;
                    }).defer(200,this);
                }
                
                return;
            }else{
                it = this.items.get(this.items.getCount() -1);
                if(it){
                    if(this.backspaceDeletesLastItem){
                        this.on('expand',function(){this.collapse();},this,{single: true});
                        it.preDestroy(true);
                    }else{
                        if(this.navigateItemsWithTab){
                            it.onElClick();
                        }else{
                            this.on('expand',function(){
                                this.collapse();
                                this.currentFocus = it;
                                this.currentFocus.onLnkFocus.defer(20,this.currentFocus);
                            },this,{single: true});
                        }
                    }
                }
                return true;
            }
        }
        
        if(!e.isNavKeyPress()){
            this.multiSelectMode = false;
            this.clearCurrentFocus();
            return;
        }
        //arrow nav
        if(e.getKey() === e.LEFT || (e.getKey() === e.UP && !this.isExpanded())){
            e.stopEvent();
            this.collapse();
            //get last item
            it = this.items.get(this.items.getCount()-1);
            if(this.navigateItemsWithTab){ 
                //focus last el
                if(it){
                    it.focus(); 
                }
            }else{
                //focus prev item
                if(this.currentFocus){
                    idx = this.items.indexOfKey(this.currentFocus.key);
                    this.clearCurrentFocus();
                    
                    if(idx !== 0){
                        this.currentFocus = this.items.itemAt(idx-1);
                        this.currentFocus.onLnkFocus();
                    }
                }else{
                    this.currentFocus = it;
                    if(it){
                        it.onLnkFocus();
                    }
                }
            }
            return true;
        }
        if(e.getKey() === e.DOWN){
            if(this.currentFocus){
                this.collapse();
                e.stopEvent();
                idx = this.items.indexOfKey(this.currentFocus.key);
                if(idx == (this.items.getCount() -1)){
                    this.clearCurrentFocus.defer(10,this);
                }else{
                    this.clearCurrentFocus();
                    this.currentFocus = this.items.itemAt(idx+1);
                    if(this.currentFocus){
                        this.currentFocus.onLnkFocus();
                    }
                }
                return true;
            }
        }
        if(e.getKey() === e.RIGHT){
            this.collapse();
            it = this.items.itemAt(0);
            if(this.navigateItemsWithTab){ 
                //focus first el
                if(it){
                    it.focus(); 
                }
            }else{
                if(this.currentFocus){
                    idx = this.items.indexOfKey(this.currentFocus.key);
                    this.clearCurrentFocus();
                    if(idx < (this.items.getCount() -1)){
                        this.currentFocus = this.items.itemAt(idx+1);
                        if(this.currentFocus){
                            this.currentFocus.onLnkFocus();
                        }
                    }
                }else{
                    this.currentFocus = it;
                    if(it){
                        it.onLnkFocus();
                    }
                }
            }
        }
    },
    onKeyUpBuffered : function(e){
        if(!e.isNavKeyPress()){
            this.autoSize();
        }
    },
    reset :  function(){
        Ext.ux.form.SuperBoxSelect.superclass.reset.call(this);
        this.addedRecords = [];
        this.autoSize().setRawValue('');
    },
    applyEmptyText : function(){
        if(this.items.getCount() > 0){
            this.el.removeClass(this.emptyClass);
            this.setRawValue('');
            return this;
        }
        if(this.rendered && this.emptyText && this.getRawValue().length < 1){
            this.setRawValue(this.emptyText);
            this.el.addClass(this.emptyClass);
        }
        return this;
    },
    /**
     * @private
     * 
     * Use clearValue instead
     */
    removeAllItems: function(){
    	this.items.each(function(item){
            item.preDestroy(true);
        },this);
        this.manageClearBtn();
        return this;
    },
    resetStore: function(){
        this.store.clearFilter();
        if(!this.removeValuesFromStore){
            return this;
        }
        this.usedRecords.each(function(rec){
            this.store.add(rec);
        },this);
        this.sortStore();
        return this;
    },
    sortStore: function(){
        var ss = this.store.getSortState();
        if(ss && ss.field){
            this.store.sort(ss.field, ss.direction);
        }
        return this;
    },
    getCaption: function(dataObject){
        if(typeof this.displayFieldTpl === 'string') {
            this.displayFieldTpl = new Ext.XTemplate(this.displayFieldTpl);
        }
        var caption, recordData = dataObject instanceof Ext.data.Record ? dataObject.data : dataObject;
      
        if(this.displayFieldTpl) {
            caption = this.displayFieldTpl.apply(recordData);
        } else if(this.displayField) {
            caption = recordData[this.displayField];
        }
        
        return caption;
    },
    addRecord : function(record) {
        var display = record.data[this.displayField],
            caption = this.getCaption(record),
            val = record.data[this.valueField],
            cls = this.classField ? record.data[this.classField] : '',
            style = this.styleField ? record.data[this.styleField] : '';

        if (this.removeValuesFromStore) {
            this.usedRecords.add(val, record);
            this.store.remove(record);
        }
        
        this.addItemBox(val, display, caption, cls, style);
        this.fireEvent('additem', this, val, record);
    },
    createRecord : function(recordData){
        if(!this.recordConstructor){
            var recordFields = [
                {name: this.valueField},
                {name: this.displayField}
            ];
            if(this.classField){
                recordFields.push({name: this.classField});
            }
            if(this.styleField){
                recordFields.push({name: this.styleField});
            }
            this.recordConstructor = Ext.data.Record.create(recordFields);
        }
        return new this.recordConstructor(recordData);
    },
    /**
     * Adds an array of items to the SuperBoxSelect component if the {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set to true.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name addItem
     * @param {Array} newItemObjects An Array of object literals containing the property names and values for an item. The property names must match those specified in {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} 
     */
    addItems : function(newItemObjects){
    	if (Ext.isArray(newItemObjects)) {
			Ext.each(newItemObjects, function(item) {
				this.addItem(item);
			}, this);
		} else {
			this.addItem(newItemObjects);
		}
    },
    /**
     * Adds an item to the SuperBoxSelect component if the {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set to true.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name addItem
     * @param {Object} newItemObject An object literal containing the property names and values for an item. The property names must match those specified in {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} 
     */
    addItem : function(newItemObject){
        var val = newItemObject[this.valueField];
        if (!val) return;

        if(this.disabled) {
            return false;
        }
        if(this.preventDuplicates && this.hasValue(val)){
            return;
        }
        
        //use existing record if found
        var record = this.findRecord(this.valueField, val);
        if (record) {
            this.addRecord(record);
            return;
        } else if (!this.allowAddNewData) { // else it's a new item
            return;
        }
        
        if(this.mode === 'remote'){
        	this.remoteLookup.push(newItemObject); 
        	this.doQuery(val,false,false);
        	return;
        }
        
        var rec = this.createRecord(newItemObject);
        this.store.add(rec);
        this.addRecord(rec);
        
        return true;
    },
    addItemBox : function(itemVal,itemDisplay,itemCaption, itemClass, itemStyle) {
        var parseStyle = function(s){
            var ret = '';
            if(typeof s == 'function'){
                ret = s.call();
            }else if(typeof s == 'object'){
                for(var p in s){
                    ret+= p +':'+s[p]+';';
                }
            }else if(typeof s == 'string'){
                ret = s + ';';
            }
            return ret;
        };
        var itemKey = Ext.id(null,'sbx-item');
        var box = new Ext.ux.form.SuperBoxSelectItem({
            owner: this,
            disabled: this.disabled,
            renderTo: this.wrapEl,
            cls: this.extraItemCls + ' ' + itemClass,
            style: parseStyle(this.extraItemStyle) + ' ' + itemStyle,
            caption: itemCaption,
            display: itemDisplay,
            value:  itemVal,
            key: itemKey,
            listeners: {
                'remove': function(item){
                    if(this.fireEvent('beforeremoveitem',this,item.value) === false){
                        return;
                    }
                    this.items.removeKey(item.key);
                    if(this.removeValuesFromStore){
                        if(this.usedRecords.containsKey(item.value)){
                            this.store.add(this.usedRecords.get(item.value));
                            this.usedRecords.removeKey(item.value);
                            this.sortStore();
                            if(this.view){
                                this.view.render();
                            }
                        }
                    }
                    if(!this.preventMultipleRemoveEvents){
                    	this.fireEvent.defer(250,this,['removeitem',this,item.value, this.findInStore(item.value)]);
                    }
                    this.preventMultipleRemoveEvents = false;
                },
                destroy: function(){
                    this.collapse();
                    this.autoSize().manageClearBtn().validateValue();
                },
                scope: this
            }
        });
        box.render();

        box.hidden = this.el.insertSibling({
            tag:'input', 
            type:'hidden', 
            value: itemVal,
            name: (this.hiddenName || this.name)
        },'before');

        this.items.add(itemKey,box);
        this.applyEmptyText().autoSize().manageClearBtn().validateValue();
    },
    manageClearBtn : function() {
        if (!this.renderFieldBtns || !this.rendered) {
            return this;
        }
        var cls = 'x-superboxselect-btn-hide';
        if (this.items.getCount() === 0) {
            this.buttonClear.addClass(cls);
        } else {
            this.buttonClear.removeClass(cls);
        }
        return this;
    },
    findInStore : function(val){
        var index = this.store.find(this.valueField, val);
        if(index > -1){
            return this.store.getAt(index);
        }
        return false;
    },
    /**
     * Returns a String value containing a concatenated list of item values. The list is concatenated with the {@link #Ext.ux.form.SuperBoxSelect-valueDelimiter}.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name getValue
     * @return {String} a String value containing a concatenated list of item values. 
     */
    getValue : function() {
        var ret = [];
        this.items.each(function(item){
            ret.push(item.value);
        });
        return ret.join(this.valueDelimiter);
    },
    /**
     * Returns an Array of item objects containing the {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} properties.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name getValueEx
     * @return {Array} an array of item objects. 
     */
    getValueEx : function() {
        var ret = [];
        this.items.each(function(item){
            var newItem = {};
            newItem[this.valueField] = item.value;
            newItem[this.displayField] = item.display;
            newItem[this.classField]= item.cls;
            ret.push(newItem);
        },this);
        return ret;
    },
    // private
    initValue : function(){
 
        Ext.ux.form.SuperBoxSelect.superclass.initValue.call(this);
        if(this.mode === 'remote') {
        	this.setOriginal = true;
        }
    },
    /**
     * Sets the value of the SuperBoxSelect component.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name setValue
     * @param {String|Array} value An array of item values, or a String value containing a delimited list of item values. (The list should be delimited with the {@link #Ext.ux.form.SuperBoxSelect-valueDelimiter) 
     */
    setValue : function(value){
        if(!this.rendered){
            this.preRenderValue = value;
            return;
        }
        
        var values = Ext.isArray(value) ? value : value.split(this.valueDelimiter);
        this.removeAllItems().resetStore();
        
        //reset remoteLookup because setValue should overwrite everything
        //inc pending data
        this.remoteLookup = [];
        
        Ext.each(values,function(val){
            var record = this.findRecord(this.valueField, val);
            if(record){
                this.addRecord(record);
            }else if(this.mode === 'remote'){
				this.remoteLookup.push(val);            	
            }
        },this);
        
        if(this.mode === 'remote'){
      		var q = this.remoteLookup.join(this.queryValuesDelimiter); 
      		this.doQuery(q,false, true); //3rd param to specify a values query
        }
        
    },
    /**
     * Sets the value of the SuperBoxSelect component, adding new items that don't exist in the data store if the {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set to true.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name setValue
     * @param {Array} data An Array of item objects containing the {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} properties.  
     */
    setValueEx : function(data){
        this.removeAllItems().resetStore();
        
        if(!Ext.isArray(data)){
            data = [data];
        }
        this.remoteLookup = [];
        
        if(this.allowAddNewData && this.mode === 'remote'){ // no need to query
            Ext.each(data, function(d){
            	var r = this.findRecord(this.valueField, d[this.valueField]) || this.createRecord(d);
                this.addRecord(r);
            },this);
            return;
        }
        
        Ext.each(data,function(item){
            this.addItem(item);
        },this);
    },
    /**
     * Returns true if the SuperBoxSelect component has a selected item with a value matching the 'val' parameter.
     * @methodOf Ext.ux.form.SuperBoxSelect
     * @name hasValue
     * @param {Mixed} val The value to test.
     * @return {Boolean} true if the component has the selected value, false otherwise.
     */
    hasValue: function(val){
        var has = false;
        this.items.each(function(item){
            if(item.value == val){
                has = true;
                return false;
            }
        },this);
        return has;
    },
    onSelect : function(record, index) {
        var val = record.data[this.valueField];
        
        if(this.preventDuplicates && this.hasValue(val)){
            return;
        }
        
        this.setRawValue('');
        this.lastSelectionText = '';
        
        if(this.fireEvent('beforeadditem',this,val) !== false){
            this.addRecord(record);
        }
        if(this.store.getCount() === 0 || !this.multiSelectMode){
            this.collapse();
        }else{
            this.restrictHeight();
        }
    },
    onDestroy : function() {
        this.items.each(function(item) {
            item.preDestroy(true);
        }, this);

        if (this.renderFieldBtns) {
            Ext.destroy(
                this.buttonClear,
                this.buttonExpand,
                this.buttonWrap
            );
        }

        Ext.destroy(
            this.inputEl,
            this.wrapEl,
            this.outerWrapEl
        );

        Ext.ux.form.SuperBoxSelect.superclass.onDestroy.call(this);
    },
    autoSize : function(){
        if(!this.rendered){
            return this;
        }
        if(!this.metrics){
            this.metrics = Ext.util.TextMetrics.createInstance(this.el);
        }
        var el = this.el,
            v = el.dom.value,
            d = document.createElement('div');

        if(v === "" && this.emptyText && this.items.getCount() < 1){
            v = this.emptyText;
        }
        d.appendChild(document.createTextNode(v));
        v = d.innerHTML;
        d = null;
        v += "&#160;";
        var w = Math.max(this.metrics.getWidth(v) +  24, 24);
        if(typeof this._width != 'undefined'){
            w = Math.min(this._width, w);
        }
        this.el.setWidth(w);
        
        if(Ext.isIE){
            this.el.dom.style.top='0';
        }
        return this;
    },
    doQuery : function(q, forceAll,valuesQuery){
        q = Ext.isEmpty(q) ? '' : q;
        var qe = {
            query: q,
            forceAll: forceAll,
            combo: this,
            cancel:false
        };
        if(this.fireEvent('beforequery', qe)===false || qe.cancel){
            return false;
        }
        q = qe.query;
        forceAll = qe.forceAll;
        if(forceAll === true || (q.length >= this.minChars)){
            if(this.lastQuery !== q){
            	this.lastQuery = q;
                if(this.mode == 'local'){
                    this.selectedIndex = -1;
                    if(forceAll){
                        this.store.clearFilter();
                    }else{
                        this.store.filter(this.displayField, q);
                    }
                    this.onLoad();
                }else{
                	
                    this.store.baseParams[this.queryParam] = q;
                    this.store.baseParams[this.queryValuesInidicator] = valuesQuery;
                    this.store.load({
                        params: this.getParams(q)
                    });
                    this.expand();
                }
            }else{
                this.selectedIndex = -1;
                this.onLoad();
            }
        }
    },
  
  // private
  onViewClick: function(doFocus) {
    var index = this.view.getSelectedIndexes()[0],
        r     = this.store.getAt(index);
    
    if (r) this.onSelect(r, index);
    if (doFocus !== false) this.el.focus();
  }
});
Ext.reg('superboxselect', Ext.ux.form.SuperBoxSelect);
/*
 * @private
 */
Ext.ux.form.SuperBoxSelectItem = function(config){
    Ext.apply(this,config);
    Ext.ux.form.SuperBoxSelectItem.superclass.constructor.call(this); 
};
/*
 * @private
 */
Ext.ux.form.SuperBoxSelectItem = Ext.extend(Ext.ux.form.SuperBoxSelectItem,Ext.Component, {
    initComponent : function(){
        Ext.ux.form.SuperBoxSelectItem.superclass.initComponent.call(this); 
    },
    onElClick : function(e){
        var o = this.owner;
        o.clearCurrentFocus().collapse();
        if(o.navigateItemsWithTab){
            this.focus();
        }else{
            o.el.dom.focus();
            var that = this;
            (function(){
                this.onLnkFocus();
                o.currentFocus = this;
            }).defer(10,this);
        }
    },
    
    onLnkClick : function(e){
        if(e) {
            e.stopEvent();
        }
        // this.preDestroy();
        if(!this.owner.navigateItemsWithTab){
            this.owner.el.focus();
        }
    },
    onLnkFocus : function(){
        this.el.addClass("x-superboxselect-item-focus");
        this.owner.outerWrapEl.addClass("x-form-focus");
    },
    
    onLnkBlur : function(){
        this.el.removeClass("x-superboxselect-item-focus");
        this.owner.outerWrapEl.removeClass("x-form-focus");
    },
    
    enableElListeners : function() {
        this.el.on('click', this.onElClick, this, {stopEvent:true});
       
        this.el.addClassOnOver('x-superboxselect-item x-superboxselect-item-hover');
    },

    enableLnkListeners : function() {
        this.lnk.on({
            click: this.onLnkClick,
            focus: this.onLnkFocus,
            blur:  this.onLnkBlur,
            scope: this
        });
    },
    
    enableAllListeners : function() {
        this.enableElListeners();
        this.enableLnkListeners();
    },
    disableAllListeners : function() {
        this.el.removeAllListeners();
        this.lnk.un('click', this.onLnkClick, this);
        this.lnk.un('focus', this.onLnkFocus, this);
        this.lnk.un('blur', this.onLnkBlur, this);
    },
    onRender : function(ct, position){
        
        Ext.ux.form.SuperBoxSelectItem.superclass.onRender.call(this, ct, position);
        
        var el = this.el;
        if(el){
            el.remove();
        }
        
        this.el = el = ct.createChild({ tag: 'li' }, ct.last());
        el.addClass('x-superboxselect-item');
        
        var btnEl = this.owner.navigateItemsWithTab ? ( Ext.isSafari ? 'button' : 'a') : 'span';
        var itemKey = this.key;
        
        Ext.apply(el, {
            focus: function(){
                var c = this.down(btnEl +'.x-superboxselect-item-close');
                if(c){
                	c.focus();
                }
            },
            preDestroy: function(){
                this.preDestroy();
            }.createDelegate(this)
        });
        
        this.enableElListeners();

        el.update(this.caption);

        var cfg = {
            tag: btnEl,
            'class': 'x-superboxselect-item-close',
            tabIndex : this.owner.navigateItemsWithTab ? '0' : '-1'
        };
        if(btnEl === 'a'){
            cfg.href = '#';
        }
        this.lnk = el.createChild(cfg);
        
        
        if(!this.disabled) {
            this.enableLnkListeners();
        }else {
            this.disableAllListeners();
        }
        
        this.on({
            disable: this.disableAllListeners,
            enable: this.enableAllListeners,
            scope: this
        });
        
        this.setupKeyMap();
    },
    setupKeyMap : function(){
        new Ext.KeyMap(this.lnk, [
            {
                key: [
                    Ext.EventObject.BACKSPACE, 
                    Ext.EventObject.DELETE, 
                    Ext.EventObject.SPACE
                ],
                fn: this.preDestroy,
                scope: this
            }, {
                key: [
                    Ext.EventObject.RIGHT,
                    Ext.EventObject.DOWN
                ],
                fn: function(){
                    this.moveFocus('right');
                },
                scope: this
            },
            {
                key: [Ext.EventObject.LEFT,Ext.EventObject.UP],
                fn: function(){
                    this.moveFocus('left');
                },
                scope: this
            },
            {
                key: [Ext.EventObject.HOME],
                fn: function(){
                    var l = this.owner.items.get(0).el.focus();
                    if(l){
                        l.el.focus();
                    }
                },
                scope: this
            },
            {
                key: [Ext.EventObject.END],
                fn: function(){
                    this.owner.el.focus();
                },
                scope: this
            },
            {
                key: Ext.EventObject.ENTER,
                fn: function(){
                }
            }
        ]).stopEvent = true;
    },
    moveFocus : function(dir) {
        var el = this.el[dir == 'left' ? 'prev' : 'next']() || this.owner.el;
	
        el.focus.defer(100,el);
    },

    preDestroy : function(supressEffect) {
    	if(this.fireEvent('remove', this) === false){
	    	return;
	    }	
    	var actionDestroy = function(){
            if(this.owner.navigateItemsWithTab){
                this.moveFocus('right');
            }
            this.hidden.remove();
            this.hidden = null;
            this.destroy();
        };
        
        if(supressEffect){
            actionDestroy.call(this);
        } else {
            this.el.hide({
                duration: 0.2,
                callback: actionDestroy,
                scope: this
            });
        }
        return this;
    },
    onDestroy : function() {
        Ext.destroy(
            this.lnk,
            this.el
        );
        
        Ext.ux.form.SuperBoxSelectItem.superclass.onDestroy.call(this);
    }
});

/*
 * Ext JS Library 2.2.1
 * Copyright(c) 2006-2009, Ext JS, LLC.
 * licensing@extjs.com
 * 
 * http://extjs.com/license
 */


Ext.form.FileUploadField = Ext.extend(Ext.form.TextField,  {
    /**
     * @cfg {String} buttonText The button text to display on the upload button (defaults to
     * 'Browse...').  Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
     * value will be used instead if available.
     */
    buttonText: 'Browse...',
    /**
     * @cfg {Boolean} buttonOnly True to display the file upload field as a button with no visible
     * text field (defaults to false).  If true, all inherited TextField members will still be available.
     */
    buttonOnly: false,
    /**
     * @cfg {Number} buttonOffset The number of pixels of space reserved between the button and the text field
     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
     */
    buttonOffset: 3,
    /**
     * @cfg {Object} buttonCfg A standard {@link Ext.Button} config object.
     */

    // private
    readOnly: true,
    
    /**
     * @hide 
     * @method autoSize
     */
    autoSize: Ext.emptyFn,
    
    // private
    initComponent: function(){
        Ext.form.FileUploadField.superclass.initComponent.call(this);
        
        this.addEvents(
            /**
             * @event fileselected
             * Fires when the underlying file input field's value has changed from the user
             * selecting a new file from the system file selection dialog.
             * @param {Ext.form.FileUploadField} this
             * @param {String} value The file value returned by the underlying file input field
             */
            'fileselected'
        );
    },
    
    // private
    onRender : function(ct, position){
        Ext.form.FileUploadField.superclass.onRender.call(this, ct, position);
        
        this.wrap = this.el.wrap({cls:'x-form-field-wrap x-form-file-wrap'});
        this.el.addClass('x-form-file-text');
        this.el.dom.removeAttribute('name');
        
        this.fileInput = this.wrap.createChild({
          id: this.getFileInputId(),
          name: this.name||this.getId(),
          cls: 'x-form-file',
          tag: 'input', 
          type: 'file',
          size: 1
        });
        
        this.button = new Kohive.IconButton({
          renderTo: this.wrap,
          iconCls:  'upload',
          cls:      'x-form-file-btn x-btn-icon'
        });
        
        if (this.buttonOnly) {
          this.el.hide();
          this.wrap.setWidth(this.button.getEl().getWidth());
        };
        
        this.fileInput.on('change', function() {
          var v = this.fileInput.dom.value;
          this.setValue(v);
          this.fireEvent('fileselected', this, v);
        }, this);
    },
    
    // private
    getFileInputId: function(){
        return this.id+'-file';
    },
    
    // private
    onResize : function(w, h){
        Ext.form.FileUploadField.superclass.onResize.call(this, w, h);
        
        this.wrap.setWidth(w);
        
        if(!this.buttonOnly){
            var w = this.wrap.getWidth() - this.button.getEl().getWidth() - this.buttonOffset;
            this.el.setWidth(w);
        }
    },
    
    // private
    preFocus : Ext.emptyFn,
    
    // private
    getResizeEl : function(){
        return this.wrap;
    },

    // private
    getPositionEl : function(){
        return this.wrap;
    },

    // private
    alignErrorIcon : function(){
        this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]);
    }
    
});
Ext.reg('fileuploadfield', Ext.form.FileUploadField);

/* SWFObject v2.1 <http://code.google.com/p/swfobject/>
	Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
	This software is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
*/
var swfobject=function(){var b="undefined",Q="object",n="Shockwave Flash",p="ShockwaveFlash.ShockwaveFlash",P="application/x-shockwave-flash",m="SWFObjectExprInst",j=window,K=document,T=navigator,o=[],N=[],i=[],d=[],J,Z=null,M=null,l=null,e=false,A=false;var h=function(){var v=typeof K.getElementById!=b&&typeof K.getElementsByTagName!=b&&typeof K.createElement!=b,AC=[0,0,0],x=null;if(typeof T.plugins!=b&&typeof T.plugins[n]==Q){x=T.plugins[n].description;if(x&&!(typeof T.mimeTypes!=b&&T.mimeTypes[P]&&!T.mimeTypes[P].enabledPlugin)){x=x.replace(/^.*\s+(\S+\s+\S+$)/,"$1");AC[0]=parseInt(x.replace(/^(.*)\..*$/,"$1"),10);AC[1]=parseInt(x.replace(/^.*\.(.*)\s.*$/,"$1"),10);AC[2]=/r/.test(x)?parseInt(x.replace(/^.*r(.*)$/,"$1"),10):0}}else{if(typeof j.ActiveXObject!=b){var y=null,AB=false;try{y=new ActiveXObject(p+".7")}catch(t){try{y=new ActiveXObject(p+".6");AC=[6,0,21];y.AllowScriptAccess="always"}catch(t){if(AC[0]==6){AB=true}}if(!AB){try{y=new ActiveXObject(p)}catch(t){}}}if(!AB&&y){try{x=y.GetVariable("$version");if(x){x=x.split(" ")[1].split(",");AC=[parseInt(x[0],10),parseInt(x[1],10),parseInt(x[2],10)]}}catch(t){}}}}var AD=T.userAgent.toLowerCase(),r=T.platform.toLowerCase(),AA=/webkit/.test(AD)?parseFloat(AD.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,q=false,z=r?/win/.test(r):/win/.test(AD),w=r?/mac/.test(r):/mac/.test(AD);/*@cc_on q=true;@if(@_win32)z=true;@elif(@_mac)w=true;@end@*/return{w3cdom:v,pv:AC,webkit:AA,ie:q,win:z,mac:w}}();var L=function(){if(!h.w3cdom){return }f(H);if(h.ie&&h.win){try{K.write("<script id=__ie_ondomload defer=true src=//:><\/script>");J=C("__ie_ondomload");if(J){I(J,"onreadystatechange",S)}}catch(q){}}if(h.webkit&&typeof K.readyState!=b){Z=setInterval(function(){if(/loaded|complete/.test(K.readyState)){E()}},10)}if(typeof K.addEventListener!=b){K.addEventListener("DOMContentLoaded",E,null)}R(E)}();function S(){if(J.readyState=="complete"){J.parentNode.removeChild(J);E()}}function E(){if(e){return }if(h.ie&&h.win){var v=a("span");try{var u=K.getElementsByTagName("body")[0].appendChild(v);u.parentNode.removeChild(u)}catch(w){return }}e=true;if(Z){clearInterval(Z);Z=null}var q=o.length;for(var r=0;r<q;r++){o[r]()}}function f(q){if(e){q()}else{o[o.length]=q}}function R(r){if(typeof j.addEventListener!=b){j.addEventListener("load",r,false)}else{if(typeof K.addEventListener!=b){K.addEventListener("load",r,false)}else{if(typeof j.attachEvent!=b){I(j,"onload",r)}else{if(typeof j.onload=="function"){var q=j.onload;j.onload=function(){q();r()}}else{j.onload=r}}}}}function H(){var t=N.length;for(var q=0;q<t;q++){var u=N[q].id;if(h.pv[0]>0){var r=C(u);if(r){N[q].width=r.getAttribute("width")?r.getAttribute("width"):"0";N[q].height=r.getAttribute("height")?r.getAttribute("height"):"0";if(c(N[q].swfVersion)){if(h.webkit&&h.webkit<312){Y(r)}W(u,true)}else{if(N[q].expressInstall&&!A&&c("6.0.65")&&(h.win||h.mac)){k(N[q])}else{O(r)}}}}else{W(u,true)}}}function Y(t){var q=t.getElementsByTagName(Q)[0];if(q){var w=a("embed"),y=q.attributes;if(y){var v=y.length;for(var u=0;u<v;u++){if(y[u].nodeName=="DATA"){w.setAttribute("src",y[u].nodeValue)}else{w.setAttribute(y[u].nodeName,y[u].nodeValue)}}}var x=q.childNodes;if(x){var z=x.length;for(var r=0;r<z;r++){if(x[r].nodeType==1&&x[r].nodeName=="PARAM"){w.setAttribute(x[r].getAttribute("name"),x[r].getAttribute("value"))}}}t.parentNode.replaceChild(w,t)}}function k(w){A=true;var u=C(w.id);if(u){if(w.altContentId){var y=C(w.altContentId);if(y){M=y;l=w.altContentId}}else{M=G(u)}if(!(/%$/.test(w.width))&&parseInt(w.width,10)<310){w.width="310"}if(!(/%$/.test(w.height))&&parseInt(w.height,10)<137){w.height="137"}K.title=K.title.slice(0,47)+" - Flash Player Installation";var z=h.ie&&h.win?"ActiveX":"PlugIn",q=K.title,r="MMredirectURL="+j.location+"&MMplayerType="+z+"&MMdoctitle="+q,x=w.id;if(h.ie&&h.win&&u.readyState!=4){var t=a("div");x+="SWFObjectNew";t.setAttribute("id",x);u.parentNode.insertBefore(t,u);u.style.display="none";var v=function(){u.parentNode.removeChild(u)};I(j,"onload",v)}U({data:w.expressInstall,id:m,width:w.width,height:w.height},{flashvars:r},x)}}function O(t){if(h.ie&&h.win&&t.readyState!=4){var r=a("div");t.parentNode.insertBefore(r,t);r.parentNode.replaceChild(G(t),r);t.style.display="none";var q=function(){t.parentNode.removeChild(t)};I(j,"onload",q)}else{t.parentNode.replaceChild(G(t),t)}}function G(v){var u=a("div");if(h.win&&h.ie){u.innerHTML=v.innerHTML}else{var r=v.getElementsByTagName(Q)[0];if(r){var w=r.childNodes;if(w){var q=w.length;for(var t=0;t<q;t++){if(!(w[t].nodeType==1&&w[t].nodeName=="PARAM")&&!(w[t].nodeType==8)){u.appendChild(w[t].cloneNode(true))}}}}}return u}function U(AG,AE,t){var q,v=C(t);if(v){if(typeof AG.id==b){AG.id=t}if(h.ie&&h.win){var AF="";for(var AB in AG){if(AG[AB]!=Object.prototype[AB]){if(AB.toLowerCase()=="data"){AE.movie=AG[AB]}else{if(AB.toLowerCase()=="styleclass"){AF+=' class="'+AG[AB]+'"'}else{if(AB.toLowerCase()!="classid"){AF+=" "+AB+'="'+AG[AB]+'"'}}}}}var AD="";for(var AA in AE){if(AE[AA]!=Object.prototype[AA]){AD+='<param name="'+AA+'" value="'+AE[AA]+'" />'}}v.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+AF+">"+AD+"</object>";i[i.length]=AG.id;q=C(AG.id)}else{if(h.webkit&&h.webkit<312){var AC=a("embed");AC.setAttribute("type",P);for(var z in AG){if(AG[z]!=Object.prototype[z]){if(z.toLowerCase()=="data"){AC.setAttribute("src",AG[z])}else{if(z.toLowerCase()=="styleclass"){AC.setAttribute("class",AG[z])}else{if(z.toLowerCase()!="classid"){AC.setAttribute(z,AG[z])}}}}}for(var y in AE){if(AE[y]!=Object.prototype[y]){if(y.toLowerCase()!="movie"){AC.setAttribute(y,AE[y])}}}v.parentNode.replaceChild(AC,v);q=AC}else{var u=a(Q);u.setAttribute("type",P);for(var x in AG){if(AG[x]!=Object.prototype[x]){if(x.toLowerCase()=="styleclass"){u.setAttribute("class",AG[x])}else{if(x.toLowerCase()!="classid"){u.setAttribute(x,AG[x])}}}}for(var w in AE){if(AE[w]!=Object.prototype[w]&&w.toLowerCase()!="movie"){F(u,w,AE[w])}}v.parentNode.replaceChild(u,v);q=u}}}return q}function F(t,q,r){var u=a("param");u.setAttribute("name",q);u.setAttribute("value",r);t.appendChild(u)}function X(r){var q=C(r);if(q&&(q.nodeName=="OBJECT"||q.nodeName=="EMBED")){if(h.ie&&h.win){if(q.readyState==4){B(r)}else{j.attachEvent("onload",function(){B(r)})}}else{q.parentNode.removeChild(q)}}}function B(t){var r=C(t);if(r){for(var q in r){if(typeof r[q]=="function"){r[q]=null}}r.parentNode.removeChild(r)}}function C(t){var q=null;try{q=K.getElementById(t)}catch(r){}return q}function a(q){return K.createElement(q)}function I(t,q,r){t.attachEvent(q,r);d[d.length]=[t,q,r]}function c(t){var r=h.pv,q=t.split(".");q[0]=parseInt(q[0],10);q[1]=parseInt(q[1],10)||0;q[2]=parseInt(q[2],10)||0;return(r[0]>q[0]||(r[0]==q[0]&&r[1]>q[1])||(r[0]==q[0]&&r[1]==q[1]&&r[2]>=q[2]))?true:false}function V(v,r){if(h.ie&&h.mac){return }var u=K.getElementsByTagName("head")[0],t=a("style");t.setAttribute("type","text/css");t.setAttribute("media","screen");if(!(h.ie&&h.win)&&typeof K.createTextNode!=b){t.appendChild(K.createTextNode(v+" {"+r+"}"))}u.appendChild(t);if(h.ie&&h.win&&typeof K.styleSheets!=b&&K.styleSheets.length>0){var q=K.styleSheets[K.styleSheets.length-1];if(typeof q.addRule==Q){q.addRule(v,r)}}}function W(t,q){var r=q?"visible":"hidden";if(e&&C(t)){C(t).style.visibility=r}else{V("#"+t,"visibility:"+r)}}function g(s){var r=/[\\\"<>\.;]/;var q=r.exec(s)!=null;return q?encodeURIComponent(s):s}var D=function(){if(h.ie&&h.win){window.attachEvent("onunload",function(){var w=d.length;for(var v=0;v<w;v++){d[v][0].detachEvent(d[v][1],d[v][2])}var t=i.length;for(var u=0;u<t;u++){X(i[u])}for(var r in h){h[r]=null}h=null;for(var q in swfobject){swfobject[q]=null}swfobject=null})}}();return{registerObject:function(u,q,t){if(!h.w3cdom||!u||!q){return }var r={};r.id=u;r.swfVersion=q;r.expressInstall=t?t:false;N[N.length]=r;W(u,false)},getObjectById:function(v){var q=null;if(h.w3cdom){var t=C(v);if(t){var u=t.getElementsByTagName(Q)[0];if(!u||(u&&typeof t.SetVariable!=b)){q=t}else{if(typeof u.SetVariable!=b){q=u}}}}return q},embedSWF:function(x,AE,AB,AD,q,w,r,z,AC){if(!h.w3cdom||!x||!AE||!AB||!AD||!q){return }AB+="";AD+="";if(c(q)){W(AE,false);var AA={};if(AC&&typeof AC===Q){for(var v in AC){if(AC[v]!=Object.prototype[v]){AA[v]=AC[v]}}}AA.data=x;AA.width=AB;AA.height=AD;var y={};if(z&&typeof z===Q){for(var u in z){if(z[u]!=Object.prototype[u]){y[u]=z[u]}}}if(r&&typeof r===Q){for(var t in r){if(r[t]!=Object.prototype[t]){if(typeof y.flashvars!=b){y.flashvars+="&"+t+"="+r[t]}else{y.flashvars=t+"="+r[t]}}}}f(function(){U(AA,y,AE);if(AA.id==AE){W(AE,true)}})}else{if(w&&!A&&c("6.0.65")&&(h.win||h.mac)){A=true;W(AE,false);f(function(){var AF={};AF.id=AF.altContentId=AE;AF.width=AB;AF.height=AD;AF.expressInstall=w;k(AF)})}}},getFlashPlayerVersion:function(){return{major:h.pv[0],minor:h.pv[1],release:h.pv[2]}},hasFlashPlayerVersion:c,createSWF:function(t,r,q){if(h.w3cdom){return U(t,r,q)}else{return undefined}},removeSWF:function(q){if(h.w3cdom){X(q)}},createCSS:function(r,q){if(h.w3cdom){V(r,q)}},addDomLoadEvent:f,addLoadEvent:R,getQueryParamValue:function(v){var u=K.location.search||K.location.hash;if(v==null){return g(u)}if(u){var t=u.substring(1).split("&");for(var r=0;r<t.length;r++){if(t[r].substring(0,t[r].indexOf("="))==v){return g(t[r].substring((t[r].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(A&&M){var q=C(m);if(q){q.parentNode.replaceChild(M,q);if(l){W(l,true);if(h.ie&&h.win){M.style.display="block"}}M=null;l=null;A=false}}}}}();

Ext.ns('Ext.ux.IRC');

/**
 * @class Ext.ux.IRC.Channel
 * @extends Ext.util.Observable
 * Represents a channel connected to
 */
Ext.ux.IRC.Channel = Ext.extend(Ext.util.Observable, {
  
  /**
   * @property joined
   * @type Boolean
   * True if this channel is currently JOINed
   */
  joined: false,
  
  /**
   * Constructor function, initializes the object
   * @param {String} name The name of the channel (e.g. #extjs)
   * @param {Ext.ux.IRC.Proxy} proxy The Ext.ux.IRC.Proxy this channel should communicate through
   */
  constructor: function(name, proxy) {
    /**
     * @property name
     * @type String
     * The name of this channel (should be set with first argument to constructor)
     */
    this.name = name;
    
    /**
     * @property proxy
     * @type Ext.ux.IRC.Proxy
     * The IRC Proxy this channel can communicate through
     */
    this.proxy = proxy;
    
    /**
     * @property messages
     * @type Array
     * An array of Ext.ux.IRC.Message objects received on this channel
     */
    this.messages = [];
    
    /**
     * @property members
     * @type Ext.util.MixedCollection
     * An array of all current members of this channel
     */
    this.members = new Ext.util.MixedCollection(false, function(item) {return item.nickname;});
    
    /**
     * @property topic
     * @type String
     * The topic assigned to this channel (defaults to an empty string until set)
     */
    this.topic = '';
    
    /**
     * Listen to events pertaining to this channel
     */
    this.onIfThisChannel(this.proxy, {
      'privmsg-received':      this.addMessage,
      'privmsg-sent':          this.addMessage,
      'name-list':             this.addMembers,
      'member-joined-channel': this.addMember,
      'member-left-channel':   this.removeMember,
      'topic-changed':         this.setTopic
    }, this);
     
    Ext.ux.IRC.Channel.superclass.constructor.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event message-received
       * Fired whenever a message is received into this channel
       * @param {Ext.ux.IRC.Member} member The member who sent the message
       * @param {Ext.ux.IRC.Message} message The message that was sent
       */
      'message-received',
      
      /**
       * @event member-joined
       * Fired whenever a member joins this channel
       * @param {Ext.ux.IRC.Member} member The Member who just joined
       */
      'member-joined',
      
      /**
       * @event member-left
       * Fired whenever a members leaves this channel
       * @param {Ext.ux.IRC.Member} member The member who just left the channel
       */
      'member-left',
      
      /**
       * @event topic-changed
       * Fired when the topic has been changed
       * @param {String} topic The new topic name
       */
      'topic-changed'
    );
  },
  
  /**
   * Sends a message to this channel via the proxy
   * @param {String} message The message to send
   */
  sendMessage: function(message) {
    this.proxy.sendPrivMsg(this.name, message);
  },
  
  /**
   * Handles events from the given Observable if this is the correct channel
   * @param {Ext.util.Observable} firingObject The event firing object to act on if this is the correct channel
   * @param {Object} listeners An object of listeners, e.g.: {'click': function() {... do something ...}}
   * @param {Object} scope The scope to call each listener in (defaults to this)
   */
  onIfThisChannel: function(firingObject, listeners, scope) {
    var scope = scope || this;
    
    for (eventName in listeners) {
      firingObject.on(eventName, function() {
        var listenerFunc = listeners[eventName];
        
        return function() {
          //the first argument is always a channel name in these events
          var channelName  = arguments[0];
          var listenerArgs = Array.prototype.slice.call(arguments, 1);
          
          this.ifThisChannel(channelName, function() {
            listenerFunc.apply(this, listenerArgs);
          });
        };
      }(), scope);
    }
  },
  
  /**
   * Calls a function if the channel name argument matches this channel's name
   * @param {String} channelName The name of the channel the message was intended for
   * @param {Function} lambda The function to run if this is the right channel
   */
  ifThisChannel: function(channelName, lambda) {
    if (channelName == this.name) {
      lambda.call(this);
    };
  },
  
  /**
   * Updates this channel's topic
   * @param {String} topic The new topic name
   */
  setTopic: function(topic) {
    this.topic = topic;
    this.fireEvent('topic-changed', topic);
  },
  
  /**
   * Creates a new Message object and adds it to the channel's messages array
   * @param {String} nickname The nickname the message came from
   * @param {String} messageText The message sent by the user
   * @return {Ext.ux.IRC.Message|null} The created message object or null if the nickname isn't found on this channel
   */
  addMessage: function(nickname, messageText) {
    var mem = this.findMember(nickname);
    
    if (mem) {
      var message = new Ext.ux.IRC.Message({member: mem, text: messageText});
      this.messages.push(message);
      
      this.fireEvent('message-received', mem, message);
    } else {
      return null;
    };
  },
  
  /**
   * Adds a member to the channel's list
   * @param {String} nickname The member's nickname
   */
  addMember: function(nickname) {
    var nickname = nickname.replace(/^@/, '');
    if (!this.findMember(nickname) && nickname.length > 0) {
      var mem = new Ext.ux.IRC.Member({nickname: nickname, proxy: this.proxy});
      this.members.add(mem);
      
      this.fireEvent('member-joined', mem);
      
      return mem;
    };
  },
  
  /**
   * Batch-adds members
   * @param {String} memberList A list of space-separated member nicknames to add
   */
  addMembers: function(memberList) {
    Ext.each(memberList.split(" "), function(name) {
      this.addMember(name);
    }, this);
  },
  
  /**
   * Removes the given member from this channel's member listing
   * @param {String} nickname The nickname of the member to remove
   */
  removeMember: function(nickname) {
    var member = this.findMember(nickname);
    
    if (member) {
      this.members.remove(member);
      this.fireEvent('member-left', member);
    };
  },
  
  /**
   * Finds a member of this channel by nickname
   * @param {String} nickname The member's nickname
   * @return {Ext.ux.IRC.Member|Null} The Member object or null
   */
  findMember: function(nickname) {
    return this.members.get(nickname);
  }
});

/**
 * @class Ext.ux.IRC.ChannelsCollection
 * @extends Ext.util.MixedCollection
 * An extended version of Ext.util.MixedCollection specialised in looking after a collection of
 * Ext.ux.IRC.Channel objects
 */
Ext.ux.IRC.ChannelsCollection = Ext.extend(Ext.util.MixedCollection, {
  
  getKey: function(item) {
    return item.name;
  },

  constructor: function() {
    
    Ext.ux.IRC.ChannelsCollection.superclass.constructor.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event message-received
       * Fires when a message is received into any of the channels in this collection
       * @param {Ext.ux.IRC.Channel} channel The channel which received the message
       * @param {Ext.ux.IRC.Member} member The Member who sent the message
       * @param {Ext.ux.IRC.Message} message The message that was received
       */
      'message-received',

      /**
       * @event member-joined
       * Fires when a member joins any of the channels in this collection
       * @param {Ext.ux.IRC.Channel} channel The channel the member just joined
       * @param {Ext.ux.IRC.Member} member The member who just joined the channel
       */
      'member-joined',

      /**
       * @event member-left
       * Fires when a member leaves any of the channels in this collection
       * @param {Ext.ux.IRC.Channel} channel The channel the member just left
       * @param {Ext.ux.IRC.Member} member The member who just left the channel
       */
      'member-left'
    );
    
    this.on('add', function(index, channel, key) {
      //re-fire the channel's message-received event, adding a reference to the channel itself
      channel.on('message-received', function(member, message) {
        this.fireEvent('message-received', channel, member, message);
      }, this);

      channel.on('member-joined', function(member) {
        this.fireEvent('member-joined', channel, member);
      }, this);

      channel.on('member-left', function(member) {
        this.fireEvent('member-left', channel, member);
      }, this);
    });
  }
});

Ext.ns('Ext.ux.IRC');

/**
 * @class Ext.ux.IRC.Manager
 * @extends Ext.util.Observable
 * Manages a set of channels connected to via an Ext.ux.IRC.Proxy
 */
Ext.ux.IRC.Manager = Ext.extend(Ext.util.Observable, {
  
  /**
   * @property autoConnect
   * @type Boolean
   * True to attempt connection to the IRC server immediately (defaults to true)
   */
  autoConnect: true,

  constructor: function(config) {
    Ext.apply(this, config);
    Ext.ux.IRC.Manager.superclass.constructor.call(this, config);
    
    /**
     * @property channels
     * @type Ext.util.MixedCollection
     * A MixedCollection of the Channels this Manager is currently JOINed to
     * relays 'message-received' events from each Channel
     */
    this.channels = new Ext.ux.IRC.ChannelsCollection();
    
    /**
     * @property proxy
     * @type Ext.ux.IRC.Proxy
     * The IRC Proxy this manager uses for its connection to the IRC server
     */
    this.proxy = new Ext.ux.IRC.Proxy(config.server);
    if (this.autoConnect) {
      this.proxy.on('flash-ready', this.proxy.connect, this.proxy);
    };
    
    this.addEvents(
      /**
       * @event channel-joined
       * Fires when a channel is joined
       * @param {Ext.ux.IRC.Channel} channel The channel object
       */
      'channel-joined'
    );
    
    this.relayEvents(this.proxy, ['connected', 'channel-left']);
    
    this.proxy.on('channel-joined', function(channelName) {
      var chan = this.findChannel(channelName);
      
      if (chan) {
        chan.joined = true;
        this.fireEvent('channel-joined', chan);
      }
    }, this);

  },
  
  /**
   * Sends a message to the specified channel
   * @param {Ext.ux.IRC.Channel} channel The Ext.ux.IRC.Channel object to send the message to
   * @param {String} messageText The text of the message to send
   */
  sendPrivMsg: function(channel, messageText) {
    this.proxy.sendPrivMsg(channel.name, messageText);
  },
  
  /**
   * Joins a new channel
   * @param {String} channel The name of the channel to join
   * @return {Ext.ux.IRC.Channel} An object representing the channel that was just joined
   */
  join: function(channel) {
    var existingChannel = this.findChannel(channel);
    
    if (existingChannel) {
      return existingChannel;
    } else {
      //join the new channel
      var newChannel = new Ext.ux.IRC.Channel(channel, this.proxy);
      this.channels.add(newChannel);
      this.proxy.join(channel);
      
      return newChannel;
    }
  },
  
  /**
   * Leaves the specified channel
   * @param {String} channelName The name of the channel to leave
   */
  leave: function(channelName) {
    var channel = this.findChannel(channelName);
    
    if (channel) {
      this.channels.remove(channel);
      this.proxy.leave(channelName);
    }
  },
  
  /**
   * Finds a channel already connected to by name
   * @param {String} channelName The name of the channel to find
   * @return {Ext.ux.IRC.Channel/Null} The Channel object for the found channel, or null
   */
  findChannel: function(channelName) {
    return this.channels.get(channelName);
  }
});

Ext.ns('Ext.ux.IRC');

Ext.ux.IRC.Member = function(config) {
  Ext.apply(this, config, {
    /**
     * @property nickname
     * @type String
     * The member's nickname (defaults to '' and should be set in constructor)
     */
    nickname: '',

    /**
     * @property hostname
     * @type String
     * The member's hostname (defaults to '' and is usually fetched by a WHOIS)
     */
    hostname: '',

    /**
     * @property username
     * @type String
     * The member's username (defaults to '' and is usually fetched by a WHOIS)
     */
    username: '',

    /**
     * @property realname
     * @type String
     * The member's real name (defaults to '' and is usually fetched by a WHOIS)
     */
    realname: ''
  });
  
  //if we're passed a proxy in the config, set up listeners to act on WHOIS responses etc
  if (this.proxy) {
    this.proxy.on('whois-response-received', this.updateFromWhois, this);
  };
};

Ext.ux.IRC.Member.prototype = {
  
  /**
   * Returns the Real Name if present, if not falls back to nickname
   * @return {String} The name to display for this member
   */
  displayName: function() {
    return this.realname.length > 0 ? this.realname : this.nickname;
  },
  
  /**
   * Updates this Member's data based on new WHOIS data
   * @param {Object} whoisData An object containing whois details (e.g. username, realname, hostname)
   */
  updateFromWhois: function(whoisData) {
    if (whoisData.nickname == this.nickname) {
      Ext.each(['hostname', 'username', 'realname'], function(prop) {
        this.setValue(prop, whoisData[prop], true);
      }, this);
    };
  },
  
  /**
   * Sets a property on this object to a new value.
   * @param {String} property The property to set (e.g. username, hostname etc)
   * @param {Mixed} value The value to set the property to
   * @param {Boolean} ignoreNull If true, does not overwrite a non-null property with a null value
   */
  setValue: function(property, value, ignoreNull) {
    ignoreNull = ignoreNull || false;
    
    if (typeof(value) != 'undefined' && ignoreNull) {
      this[property] = value;
    };
  }
};

Ext.ns('Ext.ux.IRC');

Ext.ux.IRC.Message = function(config) {
  var config = config || {};
  Ext.apply(this, config);
};

Ext.ns('Ext.ux.IRC');

Ext.ux.IRC.MessageDecoder = {
  
  withoutPrefixRegex: new RegExp(/([0-9A-Z]*)\s:(.*)/),
  
  withPrefixRegex:    new RegExp(/^([\:\_\.\-\!\@a-zA-Z0-9\.]*)\s([0-9A-Z]*)\s(.*)/),
  
  /**
   * @property whoisRegex
   * @type RegExp
   * A Regex to parse nickname, username, hostname and realname from a WHOIS response
   */
  whoisRegex: new RegExp(/([\-\_A-Za-z0-9]*)\s([\-\_A-Za-z0-9]*)\s([\-\_\.A-Za-z0-9]*)\s\*\s:([\s\-\_A-Za-z0-9]*)/),
  
  /**
   * @property commandNames
   * @type Object
   * Mapping from numeric command codes to string codes
   */
  commandNames: {
    311: 'RPL_WHOISUSER',
    332: 'RPL_TOPIC',
    353: 'RPL_NAMREPLY',
    366: 'RPL_ENDOFNAMES',
    372: 'RPL_MOTD',
    375: 'RPL_MOTDSTART',
    376: 'RPL_ENDOFMOTD'
  },
  
  decode: function(message) {
    var d = Ext.ux.IRC.MessageDecoder;
    
    var decoded = {original: message};
    
    //determine whether we're dealing with a server prefix or not
    //FIXME: Removed this because Domine IRC server isn't sending leading colons
    var hasPrefix = true; //message[0] == ':';
    
    if (hasPrefix) {
      var matches = message.match(d.withPrefixRegex);
      
      if (matches) {
        decoded['server'] = matches[1].replace(":", "");;
        decoded['params'] = matches[3];
        decoded['number'] = parseInt(matches[2], 10);
        decoded['name']   = d.commandNames[decoded['number']] || matches[2];
        
        decoded['message'] = decoded['params'].substring(decoded['params'].indexOf(':') + 1);
        
        //decode additional parameters if we're dealing with a WHOIS response
        if (decoded['name'] == 'RPL_WHOISUSER') {
          var whoisMatches = decoded['params'].match(this.whoisRegex);
          if (whoisMatches) {
            decoded['nickname'] = whoisMatches[1];
            decoded['username'] = whoisMatches[2];
            decoded['hostname'] = whoisMatches[3];
            decoded['realname'] = whoisMatches[4];
          }
        };
      }
    };
    
    return decoded;
  }
};

Ext.ns('Ext.ux.IRC');

/**
 * @class Ext.ux.IRC.Proxy
 * @extends Ext.util.Observable
 * A simple IRC client proxy.  Uses a flash object to connect to an IRC server
 */
Ext.ux.IRC.Proxy = Ext.extend(Ext.util.Observable, {
  
  /**
   * @property proxyHolderId
   * @type String
   * The ID to use for the <object> tag used to house the flash connector (defaults to 'irc_connect')
   */
  proxyHolderId: 'irc_connect',
  
  /**
   * @property serverHost
   * @type String
   * The hostname of the server to connect to.  This should be considered read-only and be sent in the constructor
   * (defaults to an empty string)
   */
  serverHost: '',
  
  /**
   * @property serverPort
   * @type Number
   * The IRC server's port number (defaults to 6667)
   */
  serverPort: 6667,
  
  /**
   * @property serverPolicyPort
   * @type Number
   * The IRC server's security policy port (defaults to 6668)
   */
  serverPolicyPort: 6668,
  
  /**
   * @property password
   * @type String
   * The password to send to the IRC server when connecting (defaults to an empty string)
   */
  password: '',
  
  /**
   * @property nickname
   * @type String
   * The nickname to connect to the server with (defaults to an empty string)
   */
  nickname: '',
  
  /**
   * @property realname
   * @type String
   * The Real Name to send to the server in USER command (defaults to an empty string)
   */
  realname: '',

  /**
   * @property proxySwfPath
   * @type String
   * The path to IrcProxy.swf
   */
  proxySwfPath: 'IrcProxy.swf',
  
  /**
   * @property showLogMessages
   * @type Boolean
   * True to log messages to the console if it is present (defaults to false)
   */
  showLogMessages: false,
  
  /**
   * @property connected
   * @type Boolean
   * True if the connection to the IRC server has been made yet
   */
  connected: false,
 
  constructor: function(config) {
    var config = config || {};
    Ext.apply(this, config);
    
    /**
     * @property flashEl
     * @type Ext.Element
     * A placeholder <object> tag created to house the flash object
     */
    this.flashEl = Ext.getBody().createChild({id: this.proxyHolderId});
    
    //load the flash up
    var cfg = {allownetworking: 'all', allowscriptaccess: 'always'};
    
    swfobject.embedSWF(this.proxySwfPath, this.proxyHolderId, "1", "1", "9.0.0", "expressInstall.swf", cfg, cfg, cfg);
    
    Ext.ux.IRC.Proxy.superclass.constructor.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event message-received
       * Fired whenever a message is received from the IRC server
       * @param {String} message The message that was received
       */
      'message-received',
      
      /**
       * @event before-message-sent
       * Fires before a message is sent to the server (return false on any listener to cancel)
       * @param {String} message The message about to be sent
       */
      'before-message-sent',
      
      /**
       * @event message-sent
       * Fires whenever a message is sent to the IRC server
       * @param {String} message The message that was sent
       */
      'message-sent',
      
      /**
       * @event privmsg-received
       * Fired when a PRIVMSG is received to one of the channels currently JOINed
       */
      'privmsg-received',
      
      /**
       * @event ping-received
       * Fires whenever a PING is received from the server
       * @param {String} message The full message received
       */
      'ping-received',
      
      /**
       * @event pong-sent
       * Fires whenever a PONG message is sent to the server
       */
      'pong-sent',
      
      /**
       * @event connected
       * Fires when a server connection has been established (at end of MOTD response)
       */
      'connected',
      
      /**
       * @event channel-joined
       * Fired when a new channel is joined by this user
       * @param {String} channel The name of the channel that was just joined
       */
      'channel-joined',
      
      /**
       * @event channel-left
       * Fired when a channel is left by this user
       * @param {String} channel The name of the channel just left
       */
      'channel-left',
      
      /**
       * @event member-joined-channel
       * Fired when a member joins the given channel (not including our member)
       * @param {String} channelName The name of the channel joined
       * @param {String} nickname The nickname of the person who joined the channel
       */
      'member-joined-channel',
      
      /**
       * @event member-left-channel
       * Fired when a member leaves the given channel (not including our member)
       * @param {String} channelName The name of the channel joined
       * @param {String} nickname The nickname of the person who joined the channel
       */
      'member-left-channel',
      
      /**
       * @event name-list
       * Fired when a names list is received from the server
       * @param {String} channelName The channel the list is for
       * @param {String} namesList The string list of names received, space separated
       */
      'name-list',
      
      /**
       * @event privmsg-received
       * Fires when a privage message is received to a channel or user
       * @param {String} channel The channel the message is sent to
       * @param {String} message The message sent
       */
      'privmsg-received',
      
      /**
       * @event privmsg-sent
       * Fires when a privage message is sent to a channel or user.  Useful for displaying messages the user sends
       * @param {String} channel The channel the message was sent to
       * @param {String} message The message sent
       */
      'privmsg-sent',
      
      /**
       * @event flash-ready
       * Fired when the flash component has signalled that it is ready.  Mainly for internal use...
       */
      'flash-ready',
      
      /**
       * @event whois-response-received
       * Fired when a WHOIS response is received after a WHOIS request has been sent
       * @param {Object} params All parameters decoded from the response.  Properties of this object useful here
       * are username, hostname, nickname and realname
       */
      'whois-response-received'
    );
    
    this.on('message-received', this.handleMessage, this);
    this.on('connected', function() {
      this.connected = true;
    }, this);
    
    this.setupHax();
  },
  
  /**
   * A collection of hacks to be used until we don't need the Flash component to query these any more
   */
  setupHax: function() {
    //a method the flash proxy seems to need to ensure everything is ready
    window.isReady = function() {return true;};
    
    //TODO: hax. Flash is replying using some hard-coded callback names at the moment,
    // one of which is window.connected
    window.connected   = this.sendCredentials.createDelegate(this);
    
    window.receiveLine = this.onLineReceived.createDelegate(this);
    window.sendMessage = this.sendMessage.createDelegate(this);
    
    //when this is called by the flash it means everything is ready
    var callReady = function() { this.fireEvent('flash-ready', this); };
    window.setSWFIsReady = callReady.createDelegate(this);
  },
  
  /**
   * @property channelMatcherRegex
   * @type RegExp
   * A regex to parse the channel name from a string
   */
  channelMatcherRegex: new RegExp(/(#[A-Za-z0-9\-\_]*)\s/),
  
  /**
   * @property userMatcherRegex
   * @type RegExp
   * A regex to parse the user name from a PRIVMSG sent straight to a user (not to a channel)
   */
  userMatcherRegex: new RegExp(/^([A-Za-z0-9\-]*)\s/),
    
  /**
   * Decodes incoming messages and fires events appropriately
   * @param {String} message The full message received from the server
   */
  handleMessage: function(message) {
    //handle PINGs as a special case
    if (message == "PING :" + this.serverHost) {
      if (this.fireEvent('ping-received', message)) {
        this.pong();
      }
    } else {
      var decoded = Ext.ux.IRC.MessageDecoder.decode(message);
      
      switch (decoded.name) {
        
        case "RPL_ENDOFMOTD" : 
          this.fireEvent('connected');
          break;
          
        case "RPL_ENDOFNAMES" : 
          var channelName = decoded.params.match(this.channelMatcherRegex)[1];
          this.fireEvent('channel-joined', channelName);
          break;
        
        case "RPL_NAMREPLY" :
          var channelName = decoded.params.match(this.channelMatcherRegex)[1];
          this.fireEvent('name-list', channelName, decoded.message);
          break;
          
        case "RPL_WHOISUSER" :
          this.fireEvent('whois-response-received', decoded);
          break;
          
        case "TOPIC" :
        case "RPL_TOPIC" :
          var channelName = decoded.params.match(this.channelMatcherRegex)[1];
          this.fireEvent('topic-changed', channelName, decoded.message);
          break;
          
        case "PRIVMSG" :
          //if a message is sent straight to a user, match on userMatch result (e.g. it won't start with a '#')
          var channelMatch = decoded.params.match(this.channelMatcherRegex);
          var userMatch    = decoded.params.match(this.userMatcherRegex);
          var channelName  = channelMatch ? channelMatch[1] : userMatch[1];
           
          var nickname     = decoded.server.split("@")[0].split("!")[0].replace(":", "");
          this.fireEvent('privmsg-received', channelName, nickname, decoded.message);
          break;
          
        case 'JOIN' :
          var nickname = decoded.server.split("@")[0].split("!")[0].replace(":", "");
          this.fireEvent('member-joined-channel', decoded.message, nickname); //decoded.message is the channel name
          break;
          
        case 'PART' :
          var nickname = decoded.server.split("@")[0].split("!")[0].replace(":", "");
          this.fireEvent('member-left-channel', decoded.message, nickname); //decoded.message is the channel name
          break;
      }
    }
  },
  
  /**
   * Sends a PONG message to the server
   */
  pong: function() {
    var message = "PONG :" + this.serverHost;
    
    this.sendMessage(message);
    this.fireEvent('pong-sent', message);
  },
  
  sendMessage: function(message) {
    this.log('sent ' + message);
    if (this.fireEvent('before-message-sent', message)) {
      this.chatServer().sendLine(message);
      
      this.fireEvent('message-sent', message);
    }
  },
  
  /**
   * This is called by flash when a new line is received.
   * DON'T override this unless you know what you're doing, instead use the 'message-received'
   * event which this object fires with the message as the first and only argument
   */
  onLineReceived: function(message) {
    this.log('received ' + message);
    this.fireEvent('message-received', message);
  },
  
  sendCredentials: function(credentials) {
    if (this.password != '') this.sendMessage("PASS " + this.password);
    this.sendMessage("NICK " + this.nickname);
    this.sendMessage("USER rar rar rar :" + this.realname);
  },
  
  /**
   * Sends a PRIVMSG to the specified channel or user
   * @param {String} channelName The name of the channel to send to
   * @param {String} messageText The text of the message to send
   */
  sendPrivMsg: function(channelName, messageText) {
    this.sendMessage("PRIVMSG " + channelName + " :" + messageText);
    this.fireEvent('privmsg-sent', channelName, this.nickname, messageText);
  },
  
  /**
   * Sends a WHOIS request to the server for the given username.  Listen to whois-response-received
   * events to receive answer as this should be considered asynchronous
   * @param {String} nickname The nickname of the user to perform a WHOIS on
   */
  whois: function(nickname) {
    this.sendMessage("WHOIS " + nickname);
  },
  
  /**
   * Joins the specified channel
   * @param {String} channel The name of the channel to join
   */
  join: function(channel) {
    this.sendMessage("JOIN " + channel);
  },
  
  /**
   * Leaves the specified channel
   * @param {String} channel The name of the channel to leave
   */
  leave: function(channel) {
    this.sendMessage("PART " + channel);
    this.fireEvent('channel-left', channel);
  },
  
  /**
   * Connects to the IRC server
   */
  connect: function() {
    var connectTask = new Ext.util.DelayedTask(function() {
      this.chatServer().connect(this.serverHost, this.serverPort, this.serverPolicyPort);
    }, this);
    
    connectTask.delay(100);
  },
  
  /**
   * Disconnects an open connection to the IRC server
   */
  disconnect: function() {
    this.sendMessage("QUIT");
  },
  
  chatServer: function() {
    return swfobject.getObjectById(this.proxyHolderId);
  },
  
  log: function(message) {
    if (this.showLogMessages === true && console != undefined && console.log != undefined) {
      console.log(message);
    };
  }

});

/**
 * @class Ext.ux.IRC.ChannelPanel
 * @extends Ext.Component
 * Represents the channels and their members managed by an Ext.ux.IRC.Manager
 */
Ext.ux.IRC.ChannelPanel = Ext.extend(Ext.Panel, {
  
  /**
   * @property baseCls
   * @type String
   * The CSS class to add to the wrapper element (defaults to 'irc-channel-panel')
   */
  baseCls: 'irc-channel-panel',
  
  /**
   * @property channelsUlCls
   * @type String
   * The CSS class to add to the <ul> element which holds the channels
   */
  channelsUlCls: 'irc-channels',
  
  /**
   * @property channelLiCls
   * @type String
   * The CSS class added to each Channel <li>.  READ-ONLY
   */
  channelLiCls: 'irc-channel',
  
  /**
   * @property membersUlCls
   * @type String
   * The CSS class to add to the <ul> elements which hold members in a given channel
   */
  membersUlCls: 'irc-members',
  
  /**
   * @property channelActiveCls
   * @type String
   * The CSS class to add to a channel <li> element when it is the active channel
   */
  channelActiveCls: 'irc-channel-active',
  
  /**
   * @property activeChannel
   * @type Ext.ux.IRC.Channel
   * The name of the currently active channel
   */
  activeChannel: '',
  
  /**
   * @property rebuildEvents
   * @type Array
   * An array of events fired by the Ext.ux.IRC.Manager that will trigger an HTML rebuild of the channels <ul>
   */
  rebuildEvents:  ['channel-joined', 'channel-left'],
  
  /**
   * @property lastSeenMessageId
   * @type Object
   * An object of channelname : numberOfMessagesSeen items.  Used to store the index of the last
   * message the user could have read in each channel based on whether the channel is active or not
   */
  lastSeenMessageId: {},

 
  initComponent: function() {
    Ext.applyIf(this, {
      html: {tag: 'ul', cls: this.channelsUlCls},
      
      /**
       * @property contextChannelName
       * @type String
       * The name of the most recently right-clicked channel - used when firing the leave-channel event
       */
      contextChannelName: ''
    });
    
    Ext.ux.IRC.ChannelPanel.superclass.initComponent.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event channel-clicked
       * Fires when a channel is clicked
       * @param {String} channelName The name of the channel that was clicked
       */
      'channel-clicked',
      
      /**
       * @event leave-channel
       * Fired when the user has indicated via the interface that they would like to leave a channel
       * Does not actually cause the channel to be left, your app needs to do that
       * @param {String} channelName The name of the channel the user would like to leave
       */
      'leave-channel'
    );
    
    ExtMVC.OS.getOS().on('channel-changed', this.setActiveChannel, this);
    
    //set up the context menu
    this.contextMenu = new Ext.menu.Menu({
      items: [
        {
          text:  'Leave this channel',
          scope: this,
          handler: function() {
            this.fireEvent('leave-channel', this.contextChannelName);
          }
        }
      ]
    });
    
    //set up click listeners once everything has been rendered
    this.on('render', function() {
      this.el.on('click',       this.handleElementClick, this);
      this.el.on('contextmenu', this.handleContextMenu,  this);
    }, this);
    
    this.addRebuildListeners();
  },
  
  /**
   * Marks the requested channel name as the active channel, and all others as inactive
   * @param {Ext.ux.IRC.Channel/String} channelName The channel to set active, or its string name
   */
  setActiveChannel: function(channelName) {
    //normalise inputs to an Ext.ux.IRC.Channel object
    if (typeof channelName == 'string') channelName = this.manager.channels.get('#' + channelName);
    
    //record the newly active channel
    this.activeChannel = channelName;
    
    //mark all messages on new channel as read
    this.setUnread(this.activeChannel, this.activeChannel.messages.length);
    
    //Rebuild the HTML and add the 'active' CSS class to the appropriate channel
    this.buildElements();
  },
  
  /**
   * Responds appropriately to clicks inside this panel
   * @param {Ext.EventObject} event The click event object
   */
  handleElementClick: function(event) {
    var name = this.getChannelFromClick(event);
    
    if (name) this.fireEvent('channel-clicked', name);
  },
  
  /**
   * Displays a context menu when a channel is right-clicked on
   * @param {Ext.EventObject} event The right-click event that was fired
   */
  handleContextMenu: function(event) {
    var name = this.getChannelFromClick(event);
    
    if (name) {
      this.contextChannelName = name;
      event.stopEvent();
      this.contextMenu.showAt(event.getXY());
    }
  },
  
  /**
   * Returns the name of the channel that was clicked on from a click event on the Element
   * @param {Ext.EventObject} event The click event
   * @return {String} The name of the channel, or null
   */
  getChannelFromClick: function(event) {
    var channel = event.getTarget('li.' + this.channelLiCls);
    if (channel) {
      return "#" + channel.id.split('-')[1];
    } else {
      return null;
    }
  },
  
  /**
   * Displays an alert showing the number of unread messages pending
   * @param {Number} number The number of messages waiting
   */
  setUnread: function(channel, number) {
    this.lastSeenMessageId[channel.name] = number;
  },
  
  /**
   * Increments the last seen count on a channel by the given number
   * @param {Number} by The number to increment by (defaults to 1)
   */
  incrementLastSeen: function(channel, by) {
    by = by || 1;
    
    this.setUnread(channel, this.lastSeenMessageId[channel.name] + by);
  },
  
  /**
   * Adds listeners to Manager events which trigger a rebuild
   */
  addRebuildListeners: function() {
    //listen to Manager events which require a rebuild
    if (this.manager) {
      Ext.each(this.rebuildEvents, function(eventName) {
        this.manager.on(eventName, this.buildElements, this);
      }, this);
      
      this.manager.channels.on('message-received', function(channel, member, message) {
        if (channel.name == this.activeChannel.name) {
          this.incrementLastSeen(channel);
        } else {
          this.buildElements();
        }
      }, this);
      
      this.manager.channels.on('member-joined', this.buildElements, this);
      this.manager.channels.on('member-left',   this.buildElements, this);
    };
  },
  
  /**
   * Builds the channel and member listings based this.manager.  Removes any existing elements
   */
  buildElements: function() {
    console.log('building');
    console.log(this);
    console.log(this.el);
    
    //clear out existing elements
    var u = this.el.child('ul.' + this.channelsUlCls);
    u.dom.innerHTML = '';
    
    this.manager.channels.each(function(channel) {
      //make sure we have a lastSeenMessageId for this channel
      this.lastSeenMessageId[channel.name] = this.lastSeenMessageId[channel.name] || 0;
      
      //calculate the number of messages not yet seen in this channel
      var unseenMessages = channel.messages.length - this.lastSeenMessageId[channel.name];
      
      //create a channel <li> element
      var chanEl = u.createChild({
        tag:      'li',
        cls:      this.channelLiCls + (channel.name == this.activeChannel.name ? ' ' + this.channelActiveCls : ''),
        id:       'channel-' + channel.name.replace('#', ''),
        children: [
          {
            tag: 'span',
            html: unseenMessages,
            cls: 'alert' + (unseenMessages == 0 ? ' hidden' : '')
          },
          {
            tag: 'h2',
            html: channel.name.replace('#', '')
          }
        ]
      });
      
      //build an array of member elements to add to this channel
      var memberEls = [];
      channel.members.each(function(member) {
        memberEls.push({tag: 'li', html: member.displayName()});
      }, this);
      
      //add the members <ul>
      var memUl = chanEl.createChild({
        tag:      'ul',
        cls:      this.membersUlCls,
        children: memberEls
      });
    }, this);
  }
});

Ext.reg('channel-panel', Ext.ux.IRC.ChannelPanel);

/**
 * @class Ext.ux.IRC.ChatPanel
 * @extends Ext.Panel
 * Displays the current chat and a send message form
 */
Ext.ux.IRC.ChatPanel = Ext.extend(Ext.Panel, {
  
  constructor: function(config) {
    config = config || {};
    
    Ext.applyIf(config, {
      historyPanelConfig    : {},
      sendMessagePanelConfig: {}
    });
    
    Ext.apply(this, {
      sendMessageInputId : 'irc-message-to-send-' + new Date().format('is'),

      sendMessageSubmitId: 'irc-message-send-button-' + new Date().format('is'),
      
      /**
       * @property sentMessages
       * @type Array
       * Array of messages sent so far this session, with newest messages first
       */
      sentMessages: [],

      /**
       * @property sentMessageHistoryIndex
       * @type Number
       * The index of the sentMessages array currently being shown in the textarea
       */
      sentMessageHistoryIndex: -1
    });
    
    this.historyPanelConfig = Ext.applyIf(config.historyPanelConfig, {
      region:     'center',
      autoScroll: true
    });
    
    this.sendMessagePanelConfig = Ext.applyIf(config.sendMessagePanelConfig, {
      region:    'south',
      height:    50,
      minHeight: 50,
      split:     true,
      
      html: {
        cls: 'irc-chat-message',
        children: [
          {
            tag:      'input',
            type:     'submit',
            value:    'Send',
            style:    'width: 20%; float: right;',
            id:       this.sendMessageSubmitId,
            tabindex: 2
          },
          {
            tag:      'textarea',
            style:    'width: 75%; height: 100%;',
            id:       this.sendMessageInputId,
            tabindex: 1   
          }
        ]
      }
    });
    
    Ext.ux.IRC.ChatPanel.superclass.constructor.apply(this, arguments);
  },

  initComponent: function() {
    /**
     * @property historyPanel
     * @type Ext.ux.IRC.HistoryPanel
     * The panel used to display chat history. Supply config options in historyPanelConfig
     */
    this.historyPanel = new Ext.ux.IRC.HistoryPanel(this.historyPanelConfig);
    
    /**
     * @property sendMessagePanel
     * @type Ext.Panel
     * The panel used to allow the user to send a message. Supply config options in sendMessagePanelConfig
     */
    this.sendMessagePanel = new Ext.Panel(this.sendMessagePanelConfig);

    Ext.applyIf(this, {
      title:  'Connecting...',
      cls:    'irc-chat',
      layout: 'border',
      items:  [this.historyPanel, this.sendMessagePanel]
    });
    
    Ext.ux.IRC.ChatPanel.superclass.initComponent.apply(this, arguments);
    
    this.on('afterlayout', this.setupTextarea, this);
    this.on('afterlayout', this.setupSendButton, this);
    
    this.addEvents(
      /**
       * @event send-message
       * Fired when the enter key is pressed inside the textarea
       * @param {String} text The text currently in the textarea
       */
      'send-message'
    );
  },
  
  /**
   * Sets up the textarea input behaviour.  Run this after render
   */
  setupTextarea: function() {
    this.msgInput = Ext.get(this.sendMessageInputId);
    
    this.msgInput.dom.onkeydown = this.handleKeyPress.createDelegate(this);
  },
  
  /**
   * Listens to clicks and keypresses on the send button
   */
  setupSendButton: function() {
    var s = Ext.get(this.sendMessageSubmitId);
    
    s.on('click', this.sendMessage, this);
  },
  
  /**
   * Listens for special key presses, fires the appropriate actions
   * @param {Ext.EventObject} event The key event
   */
  handleKeyPress: function(event) {
    var code = event.keyCode, eo = Ext.EventObject;
    
    switch (code) {
      case eo.ENTER : return this.sendMessage();    break;
      case eo.UP    : return this.historyBack();    break;
      case eo.DOWN  : return this.historyForward(); break;
    }
  },
  
  /**
   * Sends the message currently inside the textarea, adds to history
   */
  sendMessage: function() {
    var message = this.msgInput.getValue().replace(/\n/, '');
    
    //don't attempt to send an empty string
    if (!message || message.length == 0) return false;
    
    //reset history to latest message sent
    this.sentMessageHistoryIndex = -1;
    
    this.sentMessages.unshift(message);
    this.fireEvent('send-message', message);
    
    this.msgInput.dom.value = '';
    return false; //stops the enter key creating new line on textarea
  },
  
  /**
   * Shows the previous stored history message if one is present
   */
  historyBack: function() {
    if (this.showHistoryMessage(this.sentMessageHistoryIndex + 1)) {
      this.sentMessageHistoryIndex += 1;
    };
  },
  
  /**
   * Shows the next stored history message if one is present
   */
  historyForward: function() {
    if (this.showHistoryMessage(this.sentMessageHistoryIndex - 1)) {
      this.sentMessageHistoryIndex -= 1;
    };
  },
  
  /**
   * Displays a previously sent message from the message history
   * @param {Number} index The index of the sentMessages history array to display
   * @return {Boolean} True if history message successfully shown, false otherwise
   */
  showHistoryMessage: function(index) {
    var val = this.sentMessages[index];
    
    if (val) {
      this.msgInput.dom.value = val;
      return true;
    };
    
    return false;
  }
});

Ext.reg('chat_panel', Ext.ux.IRC.ChatPanel);

/**
 * @class Ext.ux.IRC.HistoryPanel
 * @extends Ext.Component
 * Displays a chat history
 */
Ext.ux.IRC.HistoryPanel = Ext.extend(Ext.Panel, {
  
  /**
   * @property urlRegex
   * @type RegExp
   * A regex to match urls in the text to allow them to be highlighted
   */
  urlRegex: new RegExp("(http:\/\/[www\.]*[a-zA-Z0-9\-\_]*\.[a-z]{2,3}[\.a-z]{3,4}[\-\/\?\=\+\.\_a-zA-Z0-9]*)", 'g'),
  
  /**
   * @property channelRegex
   * @type RegExp
   * A regex to match channel names in the text to allow them to be highlighted
   */
  channelRegex: new RegExp("(#[A-Za-z0-9]*)"),
  
  /**
   * @property autoOpenLinks
   * @type Boolean
   * If true, automatically attach an event listener to open a new window when a url is clicked in the history messages
   * (defaults to true)
   */
  autoOpenLinks: true,
  
  /**
   * @property numberOfNicknameClasses
   * @type Number
   * The number of nickname CSS classes to cycle through (e.g. 'nickname-1' up to 'nickname-x').  CSS classes can target
   * each to provide different styling for each nickname
   */
  numberOfNicknameClasses: 3,
  
  constructor: function(config) {
    config = config || {};
    
    Ext.applyIf(config, {
      ulId: 'irc-chat-history-messages-' + new Date().format('is')
    });
    
    Ext.ux.IRC.HistoryPanel.superclass.constructor.apply(this, arguments);
  },

  /**
   * Initialise everything, add our events and listeners
   */
  initComponent: function() {
    Ext.applyIf(this, {
      cls:       'irc-chat-history',
      nicknames: [],
      messages:  [],
      html: {
        tag: 'ul',
        id:  this.ulId
      },
      
      /**
       * @property currentChannel
       * @type Ext.ux.IRC.Channel
       * The channel this History panel is currently listening to
       */
      currentChannel: null
    });
    
    Ext.ux.IRC.HistoryPanel.superclass.initComponent.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event channel-clicked
       * Fired when a link to a channel name is clicked in the history panel
       * @param {String} channelName The name of the channel that was clicked (e.g. '#extjs')
       */
      'channel-clicked',
      
      /**
       * @event link-clicked
       * Fired when a link to a website url is clicked in the history panel
       * @param {String} url The url clicked
       */
      'link-clicked'
    );
    
    if (this.autoOpenLinks) {
      this.on('link-clicked', window.open, window);
    };
    
    //Attach click handler to open links in text
    this.on('render', function() {
      this.el.on('click', function(e) {
        var t;
        if (t  = e.getTarget('span.link')) {
          this.fireEvent('link-clicked', Ext.get(t).dom.innerHTML);
        }
        else if (t = e.getTarget('span.channel')) {
          this.fireEvent('channel-clicked', Ext.get(t).dom.innerHTML);
        }
      }, this);
    }, this);
  },
  
  /**
   * Updates which channel this listens to, removes all current messages and adds any already in the channel
   * @param {Ext.ux.IRC.Channel} channel The channel to switch to
   */
  setCurrentChannel: function(channel) {
    //listen to any new messages on this channel, remove listener from previous channel
    if (this.currentChannel != undefined) {
      this.currentChannel.un('message-received', this.addMessage, this);
    }
    
    this.currentChannel = channel;
    this.currentChannel.on('message-received', this.addMessage, this);
    
    //remove existing history
    var ul = Ext.get(this.ulId);
    ul.dom.innerHTML = '';
    this.nicknames = [];
    
    //add any messages we know about in this channel
    Ext.each(channel.messages, function(message) {
      this.addMessage(message.member, message);
    }, this);
  },
  
  addMessage: function(member, message) {
    message.created_at = message.created_at || new Date();
    this.addNickname(member.nickname);
    
    //match urls
    var messageMarkup = message.text.replace(this.urlRegex, "<span class='link'>$1</span>");
    
    //match channels
    messageMarkup = messageMarkup.replace(this.channelRegex, "<span class='channel'>$1</span>");
    
    var newLi = Ext.DomHelper.append(this.ulId, {
      tag: 'li', 
      children: [
        {tag: 'span', html: message.created_at.format("H:i"), cls: 'timestamp'},
        {tag: 'span', html: member.displayName() + ":", cls: 'nickname nickname-' + this.getNicknameIndex(member.nickname)},
        messageMarkup
      ]
    });
    
    this.scrollToBottom(); 
    this.fireEvent('historyUpdated', this);
  },
  
  /**
   * Scrolls to the bottom of the chat UL
   */
  scrollToBottom: function() {
    //FIXME: For some reason scroll isn't working properly as of 3.0RC1 so this is hacked instead
    Ext.get(this.ulId).parent().scrollTo('top', 900000);
  },
  
  addNickname: function(nickname) {
    if (this.nicknames.indexOf(nickname) == -1) {
      this.nicknames.push(nickname);
    }
  },
  
  getNicknameIndex: function(nickname) {
    return this.nicknames.indexOf(nickname) % this.numberOfNicknameClasses + 1;
  }
  
});

Ext.reg('history_panel', Ext.ux.IRC.HistoryPanel);

if (typeof Kohive == "undefined") {
  Ext.ns('Kohive');
};

/**
 * @class Kohive.Button
 * @extends Ext.Button
 * Kohive button.
 */
Kohive.Button = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    cls: 'x-kohive-button'
  });
  
  Kohive.Button.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.Button, Ext.Button, {
  template: new Ext.Template(
    '<div class="kohive-button">',
      '<div class="left">',
        '<div class="right">',
          '<div class="center">',
            '<button>{text}</button>',
          '</div>',
        '</div>',
      '</div>',
    '</div>'
  ),
  
  // private
  onRender : function(ct, position) {
    var btn;
    var config = {
      text:  this.text
    };
    
    btn = this.template.append(ct, config, true);
    
    this.el = btn;
    
    var btnEl = btn.child(this.buttonSelector);
    btnEl.on('focus', this.onFocus, this);
    btnEl.on('blur', this.onBlur, this);
    
    // check what colour we need
    var colorCls;
    if (this.color == "none")    { colorCls = "none"; };
    if (this.color == "red")     { colorCls = "red"; };
    if (this.color == "x-red")   { colorCls = "red"; };
    if (this.color == "green")   { colorCls = "green"; };
    if (this.color == "x-green") { colorCls = "green"; };
    if (this.color == "white")   { colorCls = "white"; };
    if (this.color == "x-white") { colorCls = "white"; };
    if (this.color == "blue")    { colorCls = "blue"; };
    if (this.color == "x-blue")  { colorCls = "blue"; };
    if (this.color == "black")   { colorCls = "black"; };
    if (this.color == "x-black") { colorCls = "black"; };
    
    // Get the colour of the button depending on the text
    if (!this.color && this.text == "Save")   { colorCls = "green"; };
    if (!this.color && this.text == "Cancel") { colorCls = "red"; };
    
    // add the class to the button
    btn.addClass(colorCls || 'green');
		
		this.btnEl = btnEl;
		
		if (this.disabled) this.el.addClass('x-disabled');
		
    this.initButtonEl(btn, btnEl);
    
    // Remove the default x-btn class
    this.removeClass('x-btn');
    
    Ext.ButtonToggleMgr.register(this);
  },
  
  setText : function(text){
    this.text = text;
    if (this.el) {
      this.el.child("button").update(text);
    };
    this.autoWidth();
  }
});
Ext.reg('kohive_button', Kohive.Button);

/**
 * @class Kohive.SmallButton
 * @extends Kohive.Button
 * Kohive small button.
 */
Kohive.SmallButton = function(config) {
  Ext.applyIf(config, {
    cls: 'x-kohive-small-button'
  });
  
  Kohive.SmallButton.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.SmallButton, Kohive.Button, {
  template: new Ext.Template(
    '<div class="kohive-small-button">',
      '<div class="left">',
        '<div class="right">',
          '<div class="center">',
            '<button>{text}</button>',
          '</div>',
        '</div>',
      '</div>',
    '</div>'
  )
});
Ext.reg('kohive_small_button', Kohive.SmallButton);

/**
 * @class Kohive.IconButton
 * @extends Kohive.Button
 * Kohive icon button.
 */
Kohive.IconButton = function(config) {
  Ext.applyIf(config, {
    cls: 'x-kohive-icon-button'
  });
  
  Kohive.IconButton.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.IconButton, Kohive.Button, {
  template: new Ext.Template(
    '<div class="kohive-icon-button">',
      '<button></button>',
    '</div>'
  ),
  
  // private
  onRender: function(ct, position) {
    Kohive.IconButton.superclass.onRender.apply(this, arguments);
    
    // Remove the default green class
    this.el.removeClass('green');
    
    // Add the icon as a class
    var btn = this.el.child('button');
    btn.addClass(this.iconCls || 'none');
  }
});
Ext.reg('kohive_icon_button', Kohive.IconButton);

/* global Ext */
/*
 * @class Ext.ux.Media,
 *        Ext.ux.MediaComponent (xtype: media),
 *        Ext.ux.MediaPanel     (xtype: mediapanel),
 *        Ext.ux.MediaWindow
 *
 * Version:  2.0
 * Author: Doug Hendricks. doug[always-At]theactivegroup.com
 * Copyright 2007-2008, Active Group, Inc.  All rights reserved.
 *
 ************************************************************************************
 *   This file is distributed on an AS IS BASIS WITHOUT ANY WARRANTY;
 *   without even the implied warranty of MERCHANTABILITY or
 *   FITNESS FOR A PARTICULAR PURPOSE.
 ************************************************************************************

 License: ux.Media classes are licensed under the terms of
 the Open Source GPL 3.0 license (details: http://www.gnu.org/licenses/gpl.html).

 Donations are welcomed: http://donate.theactivegroup.com

 Commercial use is prohibited without a Commercial License. See http://licensing.theactivegroup.com.

 Notes: the <embed> tag is NOT used(or necessary) in this implementation

 Version:  2.0
           Height/Width now honors inline style as well,
           Added Component::mediaEl(eg: 'body', 'el') for targeted media rendering.
           Added scale and status macros.
           Added px unit assertion for strict DTDs.
           Final Quicktime config.
           Adds new PDF(Iframe), Remote Desktop Connection, Silverlight, Office Web Connect-XLS (IE),
                Powerpoint, Wordpress player profiles.


 Version:  Rc1
           Adds inline media rendering within markup: <div><script>document.write(String(new Ext.ux.Media(mediaCfg)));</script></div>
           New extensible classes :
              ux.Media
              ux.MediaComponent
              ux.MediaPanel

           Solves the Firefox reinitialization problem for Ext.Components with embedded <OBJECT> tags
           when the upstream DOM is reflowed.

           See Mozilla https://bugzilla.mozilla.org/show_bug.cgi?id=262354

 Version:  .31 Fixes to canned WMV config.
 Version:  .3  New class Heirarchy.  Adds renderMedia(mediaCfg) method for refreshing
               a mediaPanels body with a new/current mediaCfg.
 Version:  .2  Adds JW FLV Player Support and enhances mediaClass defaults mechanism.
 Version:  .11 Modified width/height defaults since CSS does not seem to
                honor height/width rules
 Version:  .1  initial release

 mediaCfg: {Object}
     { mediaType : mediaClass defined by ux.Media.mediaTypes[mediaClass]
      ,url       : Url resource to load when rendered
      ,requiredVersion : may specify a specific player/plugin version (for use with inline plugin updates where implemented)
      ,loop      : (true/false) (@macro enabled)
      ,scripting : (true/false) (@macro enabled)
      ,start     : (true/false) (@macro enabled)
      ,volume    : (number%, default: 20 ) audio volume level % (@macro enabled)
      ,height    : (default: 100%) (@macro enabled)
      ,width     : (default: 100%) (@macro enabled)
      ,scale     : (default: 1) (@macro enabled)
      ,status    : (default: false) (@macro enabled)
      ,autoSize  : (true/false) If true the rendered <object> consumes 100% height/width of its
                     containing Element.  Actual container height/width are available to macro substitution
                     engine.
      ,controls  : optional: show plugins control menu (true/false) (@macro enabled)
      ,unsupportedText: (String,DomHelper cfg) Text to render if plugin is not installed/available.
      ,listeners  : {"mouseover": function() {}, .... } DOM listeners to set each time the Media is rendered.
      ,params   : { }  members/values unique to Plugin provider
     }

*/


(function(){


    /*
    * Base Media Class
    */

    Ext.ux.Media = function(config){
        Ext.apply(this,config||{});
        this.toString = this.mediaMarkup;
        this.initMedia();
        };

    var ux = Ext.ux.Media;

    ux.mediaTypes =
     {
       "PDF" : Ext.apply({  //Acrobat plugin thru release 8.0 all crash FF3
                tag     :'object'
               ,cls     : 'x-media x-media-pdf'
               ,type    : "application/pdf"
               ,data    : "@url"
               ,autoSize:true
               ,params  : { src : "@url" }

               },Ext.isIE?{
                   classid :"CLSID:CA8A9780-280D-11CF-A24D-444553540000"
                   }:false)

      ,"PDFFRAME" : {  //Most reliable method for all browsers!!
                  tag      : 'iframe'
                 ,cls      : 'x-media x-media-pdf-frame'
                 ,frameBorder : 0
                 ,style    : {overflow:'none',width:'100%',height:'100%'}
                 ,src      : "@url"
                 ,autoSize :true
        }

      ,"WMV" : Ext.apply(
              {tag      :'object'
              ,cls      : 'x-media x-media-wmv'
              ,type     : 'application/x-mplayer2'
              ,data     : "@url"
              ,autoSize : false
              ,params  : {

                  filename     : "@url"
                 ,displaysize  : 0
                 ,autostart    : "@start"
                 ,showControls : "@controls"
                 ,showStatusBar: "@status"
                 ,showaudiocontrols : true
                 ,stretchToFit  : true
                 ,Volume        :"@volume"
                 ,PlayCount     : 1

               }
               },Ext.isIE?{
                   classid :"CLSID:22d6f312-b0f6-11d0-94ab-0080c74c7e95" //default for WMP installed w/Windows
                   ,codebase:"http://activex.microsoft.com/activex/controls/mplayer/en/nsmp2inf.cab#Version=5,1,52,701"
                   ,type:'application/x-oleobject'
                   }:
                   {src:"@url"})

       /* WMV Interface Notes
        * On IE only, to retrieve the object interface (for controlling player via JS)
        * use mediaComp.getInterface().object.controls
        *
        * For all other browsers use: mediaComp.getInterface().controls
        Related DOM attributes for WMV:
        DataFormatAs :
        Name :
        URL :
        OpenState:6
        PlayState:0
        Controls :
        Settings :
        CurrentMedia:null
        MediaCollection :
        PlaylistCollection :
        VersionInfo:9.0.0.3008
        Network :
        CurrentPlaylist :
        CdromCollection :
        ClosedCaption :
        IsOnline:false
        Error :
        Status :
        Dvd :
        Enabled:true
        FullScreen:false
        EnableContextMenu:true
        UiMode:full
        StretchToFit:false
        WindowlessVideo:false
        IsRemote:false
        */

       ,"SWF" :  Ext.apply({
                  tag      :'object'
                 ,cls      : 'x-media x-media-swf'
                 ,type     : 'application/x-shockwave-flash'
                 ,scripting: 'sameDomain'
                 ,standby  : 'Loading..'
                 ,loop     :  true
                 ,start    :  false
                 ,unsupportedText : {cn:['The Adobe Flash Player is required.',{tag:'br'},{tag:'a',cn:[{tag:'img',src:'http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif'}],href:'http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash',target:'_flash'}]}
                 ,params   : {
                      movie     : "@url"
                     ,menu      : "@controls"
                     ,play      : "@start"
                     ,quality   : "high"
                     ,allowscriptaccess : "@scripting"
                     ,allownetworking : 'all'
                     ,allowfullScreen : false
                     ,bgcolor   : "#FFFFFF"
                     ,wmode     : "opaque"
                     ,loop      : "@loop"
                    }

                },Ext.isIE?
                    {classid :"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
                     codebase:"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"
                    }:
                    {data     : "@url"})


        ,"JWP" :  Ext.apply({
              tag      :'object'
             ,cls      : 'x-media x-media-swf x-media-flv'
             ,type     : 'application/x-shockwave-flash'
             ,data     : "@url"
             ,loop     :  false
             ,start    :  false
             ,params   : {
                 movie     : "@url"
                ,flashVars : {
                               autostart:'@start'
                              ,repeat   :'@loop'
                              ,height   :'@height'
                              ,width    :'@width'
                              ,id       : '@id'
                              }
                }

        },Ext.isIE?{
             classid :"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
            ,codebase:"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"
            }:false)

         /* QT references:
          * http://developer.apple.com/documentation/quicktime/Conceptual/QTScripting_JavaScript/aQTScripting_Javascro_AIntro/chapter_1_section_1.html
          */
        ,"QT" : Ext.apply({
                       tag      : 'object'
                      ,cls      : 'x-media x-media-quicktime'
                      ,type     : "video/quicktime"
                      ,style    : {position:'relative',"z-index":1 ,behavior:'url(#qt_event_source)'}
                      ,scale    : 'aspect'  // ( .5, 1, 2 , ToFit, Aspect )
                      ,unsupportedText : '<a href="http://www.apple.com/quicktime/download/">Get QuickTime</a>'
                      ,scripting : true
                      ,volume   : '50%'
                      ,data     : '@url'
                      ,params   : {
                           src          : Ext.isIE?'@url': null
                          ,href        : "http://quicktime.com"
                          ,target      : "_blank"
                          ,autoplay     : "@start"
                          ,targetcache  : true
                          ,cache        : true
                          ,wmode        : 'transparent'
                          ,controller   : "@controls"
                      ,enablejavascript : "@scripting"
                          ,loop         : '@loop'
                          ,scale        : '@scale'
                          ,volume       : '@volume'
                          ,QTSRC        : '@url'

                       }

                     },Ext.isIE?
                          { classid      :'clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B'
                           ,codebase     :'http://www.apple.com/qtactivex/qtplugin.cab#version=7,2,1,0'

                       }:
                       {
                         PLUGINSPAGE  : "http://www.apple.com/quicktime/download/"

                    })

        //For QuickTime DOM event support include this <object> tag structure in the <head> section
        ,"QTEVENTS" : {
                   tag      : 'object'
                  ,id       : 'qt_event_source'
                  ,cls      : 'x-media x-media-qtevents'
                  ,type     : "video/quicktime"
                  ,params   : {}
                  ,classid      :'clsid:CB927D12-4FF7-4a9e-A169-56E4B8A75598'
                  ,codebase     :'http://www.apple.com/qtactivex/qtplugin.cab#version=7,2,1,0'
                 }

        //WordPress Audio Player : http://wpaudioplayer.com/
        ,"WPMP3" : Ext.apply({
                       tag      : 'object'
                      ,cls      : 'x-media x-media-audio x-media-wordpress'
                      ,type     : 'application/x-shockwave-flash'
                      ,data     : '@url'
                      ,start    : true
                      ,loop     : false
                      ,params   : {
                           movie        : "@url"
                          ,width        :'@width'  //required
                          ,flashVars : {
                               autostart    : "@start"
                              ,controller   : "@controls"
                              ,enablejavascript : "@scripting"
                              ,loop         :'@loop'
                              ,scale        :'@scale'
                              ,initialvolume:'@volume'
                              ,width        :'@width'  //required
                              ,encode       : 'no'  //mp3 urls will be encoded
                              ,soundFile    : ''   //required
                          }
                       }
                    },Ext.isIE?{classid :"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"}:false)


        ,"REAL" : Ext.apply({
                tag     :'object'
               ,cls     : 'x-media x-media-real'
               ,type    : "audio/x-pn-realaudio"
               ,data    : "@url"
               ,controls: 'imagewindow,all'
               ,start   : false
               ,standby : "Loading Real Media Player components..."
               ,params   : {
                          src        : "@url"
                         ,autostart  : "@start"
                         ,center     : false
                         ,maintainaspect : true
                         ,controller : "@controls"
                         ,controls   : "@controls"
                         ,volume     :'@volume'
                         ,loop       : "@loop"
                         ,console    : "_master"
                         ,backgroundcolor : '#000000'
                         }

                },Ext.isIE?{classid :"clsid:CFCDAA03-8BE4-11CF-B84B-0020AFBBCCFA"}:false)

        ,"SVG" : {
                  tag      : 'object'
                 ,cls      : 'x-media x-media-img x-media-svg'
                 ,type     : "image/svg+xml"
                 ,data     : "@url"
                 ,params   : { src : "@url"}
        }
        ,"GIF" : {
                  tag      : 'img'
                 ,cls      : 'x-media x-media-img x-media-gif'
                 ,src     : "@url"
        }
        ,"JPEG" : {
                  tag      : 'img'
                 ,cls      : 'x-media x-media-img x-media-jpeg'
                 ,src     : "@url"
        }
        ,"JP2" :{
                  tag      : 'object'
                 ,cls      : 'x-media x-media-img x-media-jp2'
                 ,type     : "image/jpeg2000-image"
                 ,data     : "@url"
                }

        ,"PNG" : {
                  tag      : 'img'
                 ,cls      : 'x-media x-media-img x-media-png'
                 ,src     : "@url"
        }
        ,"HTM" : {
                  tag      : 'iframe'
                 ,cls      : 'x-media x-media-html'
                 ,frameBorder : 0
                 ,style    : {overflow:'auto',width:'100%',height:'100%'}
                 ,src     : "@url"
        }
        ,"TXT" : {
                  tag      : 'object'
                 ,cls      : 'x-media x-media-text'
                 ,type     : "text/plain"
                 ,style    : {overflow:'auto',width:'100%',height:'100%'}
                 ,data     : "@url"
        }
        ,"RTF" : {
                  tag      : 'object'
                 ,cls      : 'x-media x-media-rtf'
                 ,type     : "application/rtf"
                 ,style    : {overflow:'auto',width:'100%',height:'100%'}
                 ,data     : "@url"
        }
        ,"JS" : {
                  tag      : 'object'
                 ,cls      : 'x-media x-media-js'
                 ,type     : "text/javascript"
                 ,style    : {overflow:'auto',width:'100%',height:'100%'}
                 ,data     : "@url"
        }
        ,"CSS" : {
                  tag      : 'object'
                 ,cls      : 'x-media x-media-css'
                 ,type     : "text/css"
                 ,style    : {overflow:'auto',width:'100%',height:'100%'}
                 ,data     : "@url"
        }
        ,"SILVERLIGHT" : {
              tag      : 'object'
             ,cls      : 'x-media x-media-silverlight'
             ,type      :"application/ag-plugin"
             ,data     : "@url"
             ,params  : { MinRuntimeVersion: "1.0" , source : "@url" }
        }
        ,"SILVERLIGHT2" : {
              tag      : 'object'
             ,cls      : 'x-media x-media-silverlight'
             ,type      :"application/x-silverlight-2-b2"
             ,data     : "data:application/x-silverlight-2-b2,"
             ,params  : { MinRuntimeVersion: "2.0" }
             ,unsupportedText: '<a href="http://go2.microsoft.com/fwlink/?LinkID=114576&v=2.0"><img style="border-width: 0pt;" alt="Get Microsoft Silverlight" src="http://go2.microsoft.com/fwlink/?LinkID=108181"/></a>'
        }
        ,"DATAVIEW" : {
              tag      : 'object'
             ,cls      : 'x-media x-media-dataview'
             ,classid  : 'CLSID:0ECD9B64-23AA-11D0-B351-00A0C9055D8E'
             ,type     : 'application/x-oleobject'
             ,unsupportedText: 'MS Dataview Control is not installed'

        }


        ,"OWC:XLS" : Ext.apply({     //experimental IE only
              tag      : 'object'
             ,cls      : 'x-media x-media-xls'
             ,type      :"application/vnd.ms-excel"
             ,controltype: "excel"
             ,params  : { DataType : "CSVURL"
                        ,CSVURL : '@url'
                        ,DisplayTitleBar : true
                        ,AutoFit         : true
                     }
             },Ext.isIE?{
                   codebase: "file:msowc.cab"
                  ,classid :"CLSID:0002E510-0000-0000-C000-000000000046" //owc9
                 //classid :"CLSID:0002E550-0000-0000-C000-000000000046" //owc10
                 //classid :"CLSID:0002E559-0000-0000-C000-000000000046" //owc11

                 }:false)

        ,"OWC:CHART" : Ext.apply({     //experimental
              tag      : 'object'
             ,cls      : 'x-media x-media-xls'
             ,type      :"application/vnd.ms-excel"
             ,data     : "@url"
             ,params  : { DataType : "CSVURL" }
             },Ext.isIE?{
                    classid :"CLSID:0002E500-0000-0000-C000-000000000046" //owc9
                  //classid :"CLSID:0002E556-0000-0000-C000-000000000046" //owc10
                  //classid :"CLSID:0002E55D-0000-0000-C000-000000000046" //owc11
                 }:false)

        ,"OFFICE" : {
              tag      : 'object'
             ,cls      : 'x-media x-media-office'
             ,type      :"application/x-msoffice"
             ,data     : "@url"
        }
        ,"POWERPOINT" : Ext.apply({     //experimental
                      tag      : 'object'
                     ,cls      : 'x-media x-media-ppt'
                     ,type     :"application/vnd.ms-powerpoint"
                     ,file     : "@url"
                     },Ext.isIE?{classid :"CLSID:EFBD14F0-6BFB-11CF-9177-00805F8813FF"}:false)

        ,"XML" : {
              tag      : 'iframe'
             ,cls      : 'x-media x-media-xml'
             ,style    : {overflow:'auto'}
             ,src     : "@url"
        }
        ,"VLC" : {
              tag      : 'object'
             ,cls      : 'x-media x-media-vlc'
             ,type     : "application/x-vlc-plugin"
             ,version  : "VideoLAN.VLCPlugin.2"
             ,pluginspage:"http://www.videolan.org"
             ,data     : "@url"
         }
         ,"RDP" : Ext.apply({
              tag      : 'object'
             ,cls      : 'x-media x-media-rdp'
             ,type     : "application/rds"
             ,unsupportedText: "Remote Desktop Web Connection ActiveX control is required. <a target=\"_msd\" href=\"http://go.microsoft.com/fwlink/?linkid=44333\">Download it here</a>."
             ,params:{
                  Server : '@url'
                 ,Fullscreen : false
                 ,StartConnected : false
                 ,DesktopWidth : '@width'
                 ,DesktopHeight : '@height'


             }
         },Ext.isIE?{
             classid :"CLSID:9059f30f-4eb1-4bd2-9fdc-36f43a218f4a"
            ,CODEBASE :"msrdp.cab#version=5,2,3790,0"


         }:false)

    };

    var stateRE = /4$/i;
    var pollReadyState = function(media, cb, readyRE){
        //synthesize a load event for DOMs that support object.readyState
        if(media && typeof media.readyState != 'undefined'){
            (readyRE || stateRE).test(media.readyState) ? cb({type:'load'}) : pollReadyState.defer(10,null,[media,cb]);
        }
    };

    if(parseFloat(Ext.version) < 2.1){ throw "Ext.ux.Media and sub-classes are not License-Compatible with your Ext release.";}

    Ext.extend(ux, Object , {

         mediaObject     : null,
         mediaCfg        : null,
         mediaVersion    : null,
         requiredVersion : null,
         unsupportedText : null,


         /* Component Plugins Interface
           extends the Component instance with ux.Media.* interfaces
         */
         init        : function(component){

            if(component && this.getEl === undefined){
                Ext.applyIf(component,this);
            }

         },

         // private (called once by initComponent)
         initMedia      : function(){

            if(!Ext.isIE && this.initialConfig){
               //Attach the Visibility Fix to the current class Component
               new ux.VisibilityFix({
                 mode: this.visibilityCls,
                 hideMode: this.hideMode,
                 elements:this.visModeTargets || null }
               ).init(this);
             }


             /* mediarender Event is raised when the media has been inserted into the DOM.
              * The media however, may NOT be in a usable state when the event is raised.
              */
             if(this.events){
                 this.addEvents(
                      /**
                      * @event mediarender
                      * Fires immediately after the markup has been rendered.
                      * @param {Object} This Media Class object instance.
                      * @param {Object} mediaObject The {@link Ext.Element} object rendered.
                      */

                     'mediarender',
                      /**
                      * @event mediarender
                      * Fires when the mediaObject has reported a loaded state (IE, Opera Only)
                      * @param {Object} This Media Class object instance.
                      * @param {Object} mediaObject The {@link Ext.Element} object loaded.
                      */
                     'mediaload'

                     );
              }
         },

         getMediaType: function(type){
             return ux.mediaTypes[type];
         },

         //Assert default values and exec as functions
         assert : function(v,def){
              v= typeof v === 'function'?v.call(v.scope||null):v;
              return Ext.value(v ,def);
         },

         mediaMarkup : function(mediaCfg, width, height, ct){

             mediaCfg = mediaCfg ||this.mediaCfg;

             if(!mediaCfg){return '';}

             var m= Ext.apply({url:false,autoSize:false}, mediaCfg); //make a copy

             m.url = this.assert(m.url,false);

             if( m.mediaType){

                 var value,p, El = Ext.Element;

                 var media = Ext.apply({}, this.getMediaType(this.assert(m.mediaType,false)) || false );

                 var params = Ext.apply(media.params||{},m.params || {});


                 for(var key in params){

                    if(params.hasOwnProperty(key)){
                      m.children || (m.children = []);
                      p = this.assert(params[key],null);
                      if(p !== null){
                         m.children.push({tag:'param'
                                         ,name:key
                                         ,value: typeof p === 'object'?Ext.urlEncode(p):encodeURI(p)
                                         });
                      }
                    }
                 }
                 delete   media.params;

                 //childNode Text if plugin/object is not installed.
                 var unsup = this.assert(m.unsupportedText|| this.unsupportedText || media.unsupportedText,null);
                 if(unsup){
                     m.children || (m.children = []);
                     m.children.push(unsup);
                 }

                 if(m.style && typeof m.style != "object") { throw 'Style must be JSON formatted'; }

                 m.style = this.assert(Ext.apply(media.style || {}, m.style || {}) , {});
                 delete media.style;

                 m.height = this.assert(height || m.height || media.height || m.style.height, null);
                 m.width  = this.assert(width  || m.width  || media.width || m.style.width , null);



                 m = Ext.apply({tag:'object'},m,media);


                 //Convert element height and width to inline style to avoid issues with display:none;
                 if(m.height || m.autoSize)
                 {
                    Ext.apply(m.style, {
                      height:El.addUnits( m.autoSize ? '100%' : m.height ,El.prototype.defaultUnit)});
                 }
                 if(m.width || m.autoSize)
                 {
                    Ext.apply(m.style, {
                      width :El.addUnits( m.autoSize ? '100%' : m.width ,El.prototype.defaultUnit)});
                 }

                 m.id || (m.id = Ext.id());
                 m.name = this.assert(m.name, m.id);

                 var _macros= {
                   url       : m.url || ''
                  ,height    : (/%$/.test(m.height)) ? m.height : parseInt(m.height,10)||100
                  ,width     : (/%$/.test(m.width)) ? m.width : parseInt(m.width,10)||100
                  ,scripting : this.assert(m.scripting,false)
                  ,controls  : this.assert(m.controls,false)
                  ,scale     : this.assert(m.scale,1)
                  ,status    : this.assert(m.status,false)
                  ,start     : this.assert(m.start, false)
                  ,loop      : this.assert(m.loop, false)
                  ,volume    : this.assert(m.volume, 20)
                  ,id        : m.id
                 };

                 delete   m.url;
                 delete   m.mediaType;
                 delete   m.controls;
                 delete   m.status;
                 delete   m.start;
                 delete   m.loop;
                 delete   m.scale;
                 delete   m.scripting;
                 delete   m.volume;
                 delete   m.autoSize;
                 delete   m.params;
                 delete   m.unsupportedText;
                 delete   m.renderOnResize;
                 delete   m.listeners;
                 delete   m.height;
                 delete   m.width;


                 return Ext.DomHelper.markup(m)
                   .replace(/(%40url|@url)/g          ,_macros.url)
                   .replace(/(%40start|@start)/g      ,_macros.start+'')
                   .replace(/(%40controls|@controls)/g,_macros.controls+'')
                   .replace(/(%40scale|@scale)/g      ,_macros.scale+'')
                   .replace(/(%40status|@status)/g    ,_macros.status+'')
                   .replace(/(%40id|@id)/g            ,_macros.id+'')
                   .replace(/(%40loop|@loop)/g        ,_macros.loop+'')
                   .replace(/(%40volume|@volume)/g    ,_macros.volume+'')
                   .replace(/(%40scripting|@scripting)/g ,_macros.scripting+'')
                   .replace(/(%40width|@width)/g      ,_macros.width+'')
                   .replace(/(%40height|@height)/g    ,_macros.height+'');

             }else{
                 var unsup = this.assert(m.unsupportedText|| this.unsupportedText || media.unsupportedText,null);
                 unsup = unsup ? Ext.DomHelper.markup(unsup): null;
                 return String.format(unsup || 'Media Configuration/Plugin Error',' ',' ');
             }

         }
         //Private
         ,setMask  : function(el) {

             if(this.mediaMask && !this.mediaMask.enable){
                    el = Ext.get(el);
                    if(this.mediaMask = new Ext.ux.IntelliMask(el || this[this.mediaEl],
                       Ext.apply({fixElementForMedia:true},this.mediaMask))){
                            this.mediaMask.el.addClass('x-media-mask');
                      }

            }


         }
          /*
          *  This method updates the target Element with a new mediaCfg object,
          *  or supports a refresh of the target based on the current mediaCfg object
          *  This method may be invoked inline (in Markup) before the DOM is ready
          *  param position indicate the DomHeper position for Element insertion (ie 'afterbegin' the default)
          */
          ,renderMedia : function(mediaCfg, ct, domPosition , w , h){
              if(!Ext.isReady){
                  Ext.onReady(this.renderMedia.createDelegate(this,Array.prototype.slice.call(arguments,0)));
                  return;
              }
              var mc = (this.mediaCfg = mediaCfg || this.mediaCfg) ;
              ct = Ext.get(ct || this.lastCt || (this.mediaObject?this.mediaObject.dom.parentNode:null));

              this.onBeforeMedia.call(this, mc, ct, domPosition , w , h);

              if(ct){
                  this.lastCt = ct;
                  var markup;

                  if(mc && (markup = this.mediaMarkup(mc, w, h, ct))){
                     this.setMask(ct);
                     this.clearMedia();

                     ct.update(markup);
                     if(this.mediaMask && this.autoMask){
                          this.mediaMask.show();
                     }

                  }
              }
              this.onAfterMedia(ct);


          }
          //Remove an existing mediaObject from the DOM.
          ,clearMedia : function(){
            if(Ext.isReady && this.mediaObject){
              try{
                this.mediaObject.removeAllListeners();
                this.mediaObject.remove();
              }catch(er){}
            }
            this.mediaObject = null;
          }

          ,onBeforeMedia  : function(mediaCfg, ct, domPosition ){

            var m = mediaCfg || this.mediaCfg, mt;

            if( m && (mt = this.getMediaType(m.mediaType)) ){
                m.autoSize = m.autoSize || mt.autoSize===true;

                //Calculate parent container size for macros (if available)
                if(m.autoSize && (ct = Ext.isReady?Ext.get(ct||this.lastCt):null)){
                  m.height = ct.getHeight(true) || this.assert(m.height,'auto');
                  m.width  = ct.getWidth(true)  || this.assert(m.width ,'auto');
                }

             }


          },
          // Private
          // Media Load Handler, called when a mediaObject reports a loaded readystate
          onMediaLoad : function(e){
               if(e && e.type == 'load'){
                  this.fireEvent('mediaload',this, this.mediaObject);
                  if(this.mediaMask && this.autoMask){ this.mediaMask.hide(); }
               }
          },
          onAfterMedia   : function(ct){

               if(this.mediaCfg && ct && (this.mediaObject = ct.child('.x-media') )){
                   this.mediaObject.ownerCt = this;

                    //Load detection for non-<object> media (iframe, img)
                   if(this.mediaCfg.tag !== 'object'){
                      this.mediaObject.on({
                       load  :this.onMediaLoad
                      ,scope:this
                      ,single:true
                     });
                   }

                   //IE, Opera possibly others, support a readyState on <object>s
                   pollReadyState(this.mediaObject.dom, this.onMediaLoad.createDelegate(this));

                   var L; //Reattach any DOM Listeners after rendering.
                   if(L = this.mediaCfg.listeners ||null){
                       this.mediaObject.on(L);  //set any DOM listeners
                     }
                   this.fireEvent('mediarender',this, this.mediaObject);


               }
           }

         ,getInterface  : function(){
              return this.mediaObject?this.mediaObject.dom||null:null;
          }

         ,detectVersion  : Ext.emptyFn


         /* Class default: IE provides sufficient DOM readyState for object tags to manage mediaMasks automatically
            (No other browser does), so masking must either be directed manually or use the autoHide option
            of the Ext.ux.IntelliMask.
          * if true the Component attempts to manage the mediaMask based on events/media status
          * false to control mask visibility manually.
          */

         ,autoMask   : Ext.isIE

    });

    //Private adapter class
    var mediaComponentAdapter = function(){};

    Ext.extend(mediaComponentAdapter, Object, {

         hideMode      : !Ext.isIE?'nosize':'display'

        ,animCollapse  : false

        ,visibilityCls : !Ext.isIE ?'x-hide-nosize':null

        ,autoScroll    : true

        ,shadow        : false

        ,bodyStyle     : {position: 'relative'}

        ,resizeMedia   : function(comp, w, h){
            var mc = this.mediaCfg;

            if(mc && this.boxReady){

                // Ext.Window.resizer fires this event a second time
                if(arguments.length > 3 && (!this.mediaObject || mc.renderOnResize )){
                    this.refreshMedia(this[this.mediaEl]);
                }
            }

         }

        ,onAfterRender: function(visModeTargets){

            if(this.mediaCfg.renderOnResize ){
                this.on('resize', this.resizeMedia, this);
            }else{
                this.renderMedia(this.mediaCfg,this[this.mediaEl] || this.getEl());
            }
        }

        ,doAutoLoad : Ext.emptyFn

        ,refreshMedia  : function(target){
            if(this.mediaCfg) {this.renderMedia(null,target);}
        }
        ,mediaMask  : false

    });

    /* Generic Media BoxComponent */
    Ext.ux.MediaComponent = Ext.extend(Ext.BoxComponent, {
        ctype           : "Ext.ux.MediaComponent",
        autoEl          : 'div',
        cls             : "x-media-comp",
        mediaEl         : 'el',   //the containing target element for the media,

        getId           : function(){
            return this.id || (this.id = "media-comp" + (++Ext.Component.AUTO_ID));
        },
        initComponent   : function(){
            this.visModeTargets  = [this.actionMode];
            this.initMedia();

            Ext.ux.MediaComponent.superclass.initComponent.apply(this,arguments);
        },

        onRender  : function(){

              Ext.ux.MediaComponent.superclass.onRender.apply(this, arguments);
              this.onAfterRender();

        },

        afterRender     :  function(ct){

            this.setAutoScroll();
            Ext.ux.MediaComponent.superclass.afterRender.apply(this,arguments);


        },
        beforeDestroy   : function(){
            this.clearMedia();
            Ext.destroy(this.mediaMask,this.loadMask);
            Ext.ux.MediaComponent.superclass.beforeDestroy.call(this);
         },
         //private
        setAutoScroll   : function(){
            if(this.rendered && this.autoScroll){
                this.getEl().setOverflow('auto');
            }
        }

    });

    //Inherit the Media.Flash class interface
    Ext.apply(Ext.ux.MediaComponent.prototype, Ext.ux.Media.prototype);
    Ext.apply(Ext.ux.MediaComponent.prototype, mediaComponentAdapter.prototype);
    Ext.reg('media', Ext.ux.MediaComponent);

    ux.Panel = Ext.extend( Ext.Panel, {

         ctype         : "Ext.ux.Media.Panel"
        ,cls           : "x-media-panel"
        ,mediaEl       : 'body'   //the containing target element for the media

        ,initComponent : function(){
            this.visModeTargets  = [this.collapseEl,this.floating? null: this.actionMode];
            this.initMedia();
            this.html = this.contentEl = this.items = null;

            ux.Panel.superclass.initComponent.call(this);
        }
        ,onRender  : function(){

              ux.Panel.superclass.onRender.apply(this, arguments);
              this.onAfterRender();

         }
        ,beforeDestroy : function(){
            this.clearMedia();
            ux.Panel.superclass.beforeDestroy.call(this);
         }
    });

    //Inherit the Media class interface
    Ext.apply(ux.Panel.prototype,ux.prototype);
    Ext.apply(ux.Panel.prototype, mediaComponentAdapter.prototype);
    Ext.reg('mediapanel', Ext.ux.MediaPanel = ux.Panel);

    ux.Window = Ext.extend( Ext.Window ,{

         cls           : "x-media-window"
        ,ctype         : "Ext.ux.Media.Window"
        ,mediaEl       : 'body'   //the containing target element for the media
        ,initComponent : function(){

            this.visModeTargets  = [this.collapseEl,this.floating? null: this.actionMode];
            this.initMedia();
            this.html = this.contentEl = this.items = null;

            ux.Window.superclass.initComponent.call(this);
        }

        ,onRender  : function(){
           ux.Window.superclass.onRender.apply(this, arguments);
           this.onAfterRender();
         }
         ,beforeDestroy : function(){
            this.clearMedia();
            ux.Window.superclass.beforeDestroy.call(this);
         }

    });

    //Inherit the Media class interface
    Ext.apply(ux.Window.prototype, ux.prototype);
    //then the Adaptor
    Ext.apply(ux.Window.prototype, mediaComponentAdapter.prototype);
    Ext.reg('mediawindow', Ext.ux.MediaWindow = ux.Window);


    Ext.onReady(function(){
        //Generate CSS Rules if not defined in markup
        var CSS = Ext.util.CSS, rules=[];
        CSS.getRule('.x-media') || (rules.push('.x-media{width:100%;height:100%;display:block;overflow:none;outline:none;}'));
        CSS.getRule('.x-media-mask') || (rules.push('.x-media-mask{width:100%;height:100%;position:relative;zoom:1;}'));

        //default Rule for IMG:  h/w: auto;
        CSS.getRule('.x-media-img') || (rules.push('.x-media-img{background-color:transparent;width:auto;height:auto;zoom:1;}'));

        /* These two important rule solves many of the <object/iframe>.reInit issues encountered
         * when setting display:none on an upstream(parent) element (on all Browsers except IE).
         * This default rule enables the new Panel:hideMode 'nosize'. The rule is designed to
         * set height/width to 0 cia CSS if hidden or collapsed.
         * Additional selectors also hide 'x-panel-body's within layouts to prevent
         * container and <object, img, iframe> bleed-thru.  (Also see ux.VisibilityFix below)
         */
        CSS.getRule('.x-hide-nosize') || (rules.push('.x-hide-nosize,.x-hide-nosize *{height:0px!important;width:0px!important;border:none!important;}'));

        if(!!rules.length){
             CSS.createStyleSheet(rules.join(''));
        }

    });

  /* overrides add a third visibility feature to Ext.Element:
    * Now an elements' visibility may be handled by application of a custom (hiding) CSS className.
    * The class is removed to make the Element visible again
    */

    Ext.apply(Ext.Element.prototype, {
      setVisible : function(visible, animate){
            if(!animate || !Ext.lib.Anim){
                if(this.visibilityMode === Ext.Element.DISPLAY){
                    this.setDisplayed(visible);
                }else if(this.visibilityMode === Ext.Element.VISIBILITY){
                    this.fixDisplay();
                    this.dom.style.visibility = visible ? "visible" : "hidden";
                }else {
                    this[visible?'removeClass':'addClass'](String(this.visibilityMode));
                }

            }else{
                // closure for composites
                var dom = this.dom;
                var visMode = this.visibilityMode;

                if(visible){
                    this.setOpacity(.01);
                    this.setVisible(true);
                }
                this.anim({opacity: { to: (visible?1:0) }},
                      this.preanim(arguments, 1),
                      null, .35, 'easeIn', function(){

                         if(!visible){
                             if(visMode === Ext.Element.DISPLAY){
                                 dom.style.display = "none";
                             }else if(visMode === Ext.Element.VISIBILITY){
                                 dom.style.visibility = "hidden";
                             }else {
                                 Ext.get(dom).addClass(String(visMode));
                             }
                             Ext.get(dom).setOpacity(1);
                         }
                     });
            }
            return this;
        },
        /**
         * Checks whether the element is currently visible using both visibility and display properties.
         * @param {Boolean} deep (optional) True to walk the dom and see if parent elements are hidden (defaults to false)
         * @return {Boolean} True if the element is currently visible, else false
         */
        isVisible : function(deep) {
            var vis = !(this.getStyle("visibility") === "hidden" || this.getStyle("display") === "none" || this.hasClass(this.visibilityMode));
            if(deep !== true || !vis){
                return vis;
            }
            var p = this.dom.parentNode;
            while(p && p.tagName.toLowerCase() !== "body"){
                if(!Ext.fly(p, '_isVisible').isVisible()){
                    return false;
                }
                p = p.parentNode;
            }
            return true;
        } });

        var ElementMaskFixes = {
          mask : function(msg, msgCls,maskCls){
            if(this.getStyle("position") == "static"){
                this.setStyle("position", "relative");
            }
            if(this._maskMsg){
                this._maskMsg.remove();
            }
            if(this._mask){
                //this._mask.remove();
            }

            this._mask || (this._mask = Ext.DomHelper.append(this.dom, {cls:maskCls || "ext-el-mask"}, true));

            //removed, causes DOM reflow on non-IE browsers when
            //overflow:hidden is applied to <object>'s parent Element
            //and <object>'s go to sleep when visibility:hidden is applied.

            !Ext.isIE || this.addClass("x-masked");

            this._mask.setVisible(true);

            if(typeof msg == 'string'){

                var mm = this._maskMsg = Ext.DomHelper.append(this.dom,
                 {
                     cls:"ext-el-mask-msg " + msgCls || ''
                    ,style:{
                        visibility:'hidden'
                     }
                     ,cn:{tag:'div',html:msg }
                    }, true);

                 var el = this.dom;
                 (function(){
                    try{ mm.center(el).setVisible(true); } catch(e){}
                 }).defer(4);

            }
            if(Ext.isIE && !(Ext.isIE7 && Ext.isStrict) && this.getStyle('height') == 'auto'){ // ie will not expand full height automatically
                   this._mask.setSize(this.dom.clientWidth, this.getHeight());
            }
            return this._mask;
        },
        /**
         * Removes a previously applied mask.
         */
        unmask : function(remove){

            if(this._maskMsg){
                if(remove){
                    this._maskMsg.remove();
                    delete this._maskMsg;
                } else {
                    this._maskMsg.setVisible.defer(4,this._maskMsg,[false]);
                }
            }
            if(this._mask){
                 if(remove){
                    this._mask.remove();
                    delete this._mask;
                } else {
                    this._mask.setVisible(false);
                }
            }

            this.removeClass("x-masked");
       }
     };



    /* Ext.ux.Media.VisibilityFix plugin.

      This plugin provides an alternate visibility mode to Components that support hideMode.
      If included in a Component, it sets the hideMode

      config options
           elements(array,optional)      :  [list of component members to also adjust visibility for]  (eg. ['bwrap','toptoolbar']
             mode (string,optional)      :   a specific CSS classname (or 1 or 2 ) to use for custom visibility
           hideMide (string;default 'nosize'):  the Component hideMode to assign

       Examples:

       .add({
           xtype:'panel',
           plugins: [new Ext.ux.Media.VisibilityFix({mode:'x-hide-nosize'}) ],
           ...
         });

         //or, enable on parent and all child items:

         var V = new Ext.ux.Media.VisibilityFix({mode:'x-hide-nosize'});
         new Ext.TabPanel({
            plugins: V,
            defaults: {
                plugins: V
            },
            items:[....]
         });
    */

    ux.VisibilityFix = function(opt) {
        opt||(opt={});

        this.init = function(c) {

               c.hideMode = opt.hideMode || c.hideMode;

               c.on('render',
                 function(co){

                    var els = [co.collapseEl, (co.floating? null: co.actionMode)].concat(opt.elements||[]);
                    var El = Ext.Element;

                    var mode = opt.mode || co.visibilityCls || El[co.hideMode.toUpperCase()] || El.VISIBILITY;

                    Ext.each(els, function(el){
                        var e = co[el] || el;
                        if(e && e.setVisibilityMode){e.setVisibilityMode(mode);}
                    });

                 },
                 c,
                 {single:true}
               );

        };


    };

    /**
     * @class Ext.ux.Media.MediaMask
     * A custom utility class for generically masking elements while loading media.
     * @constructor
     * Create a new LoadMask
     * @param {Mixed} el The element or DOM node, or its id
     * @param {Object} config The config object
     */
    Ext.ux.IntelliMask = function(el, config){

        Ext.apply(this, config);
        this.el = Ext.get(el);
        this.removeMask = Ext.value(this.removeMask, true);

        if(el && this.fixElementForMedia){
            Ext.apply(el, ElementMaskFixes );
        }

    };

    Ext.ux.IntelliMask.prototype = {

        /**
         * @cfg {Boolean} removeMask
         * True to create a single-use mask that is automatically destroyed after loading (useful for page loads),
         * False to persist the mask element reference for multiple uses (e.g., for paged data widgets).  Defaults to false.
         */
        /**
         * @cfg {String} msg
         * The text to display in a centered loading message box (defaults to 'Loading Media...')
         */
        msg : 'Loading Media...',
        /**
         * @cfg {String} msgCls
         * The CSS class to apply to the loading message element (defaults to "x-mask-loading")
         */
        msgCls : 'x-mask-loading',


        /** @cfg {Number} zIndex
         * the optional zIndex applied to the masking Elements
         */
        zIndex : null,

        /**
         * Read-only. True if the mask is currently disabled so that it will not be displayed (defaults to false)
         * @type Boolean
         */
        disabled: false,

        /**
         * Read-only. True if the mask is currently applied to the element.
         * @type Boolean
         */
        active: false,

        /**
         * True or millisecond value hides the mask if the @hide method is not called within the specified time limit.
         * @cfg {Boolean/Integer} (true, millisecond timeout, false)
         */
        autoHide: false,

        /**
         * Disables the mask to prevent it from being displayed
         */
        disable : function(){
           this.disabled = true;
        },

        /**
         * Enables the mask so that it can be displayed
         */
        enable : function(){
            this.disabled = false;
        },

        /**
         * Show this Mask over the configured Element.
         * Typical usage:

           mask.show({autoHide:3000});   //show defaults and hide after 3 seconds.

           mask.show('Loading Content', null, loadContentFn); //show msg and execute fn

           mask.show({
               msg: 'Loading Content',
               msgCls : 'x-media-loading',
               fn : loadContentFn,
               fnDelay : 100,
               scope : window,
               autoHide : 2000   //remove the mask after two seconds.
           });
         */
        show: function(msg, msgCls, fn, fnDelay ){
            if(this.disabled || !this.el){
                return null;
            }
            var opt={}, autoHide = this.autoHide;
            fnDelay = parseInt(fnDelay,10) || 20; //ms delay to allow mask to quiesce if fn specified

            if(typeof msg == 'object'){
                opt = msg;
                msg = opt.msg;
                msgCls = opt.msgCls;
                fn = opt.fn;
                autoHide = typeof opt.autoHide != 'undefined' ?  opt.autoHide : autoHide;
                fnDelay = opt.fnDelay || fnDelay ;
            }

            var mask = this.el.mask(msg || this.msg, msgCls || this.msgCls);

            this.active = !!this.el._mask;
            if(this.active){
                if(this.zIndex){
                    this.el._mask.setStyle("z-index", this.zIndex);
                    if(this.el._maskMsg){
                        this.el._maskMsg.setStyle("z-index", this.zIndex+1);
                    }
                }
            }
            if(typeof fn === 'function'){
                fn.defer(fnDelay ,opt.scope || null);
            } else { fnDelay = 0; }

            if(autoHide && (autoHide = parseInt(autoHide , 10)||2000)){
                this.hide.defer(autoHide+(fnDelay ||0),this);
            }

            return this.active? {mask: this.el._mask , maskMsg: this.el._maskMsg} : null;
        },

        /**
         * Hide this mediaMask.
         */
        hide: function(remove){
            if(this.el){
                this.el.unmask(remove || this.removeMask);
            }
            this.active = false;
            return this;
        },

        // private
        destroy : function(){this.hide(true); this.el = null; }
     };

})();

/* global Ext */
/*
 * @class
 *        Ext.ux.Media.Flash,
 *        Ext.ux.FlashComponent (xtype: uxflash)
 *        Ext.ux.FlashPanel     (xtype: uxflashpanel, flashpanel)
 *        Ext.ux.FlashWindow
 *
 * Version:  2.0
 * Author: Doug Hendricks. doug[always-At]theactivegroup.com
 * Copyright 2007-2008, Active Group, Inc.  All rights reserved.
 *
 ************************************************************************************
 *   This file is distributed on an AS IS BASIS WITHOUT ANY WARRANTY;
 *   without even the implied warranty of MERCHANTABILITY or
 *   FITNESS FOR A PARTICULAR PURPOSE.
 ************************************************************************************

 License: ux.Media.Flash classes are licensed under the terms of
 the Open Source GPL 3.0 license (details: http://www.gnu.org/licenses/gpl.html).

 Commercial use is prohibited without a Commercial License. See http://licensing.theactivegroup.com.

 Donations are welcomed: http://donate.theactivegroup.com

 Notes: the <embed> tag is NOT used(or necessary) in this implementation

 Version:
        1.0
           Addresses the Flash visibility/re-initialization issues for all browsers.
           Adds bi-directional fscommand support for all A-Grade browsers.


        Rc1
           Adds inline media rendering within markup: <div><script>document.write(String(new Ext.ux.Media.Flash(mediaCfg)));</script></div>
           New extensible classes :
              ux.Media.Flash
              ux.FlashComponent
              ux.FlashPanel
              ux.FlashWindow

   A custom implementation for advanced Flash object interaction
       Supports:
            version detection,
            version assertion,
            Flash Express Installation (inplace version upgrades),
            and custom Event Sync for interaction with SWF.ActionScript.

    mediaCfg: {Object}
         {
           url       : Url resource to load when rendered
          ,loop      : (true/false)
          ,start     : (true/false)
          ,height    : (defaults 100%)
          ,width     : (defaults 100%)
          ,scripting : (true/false) (@macro enabled)
          ,controls  : optional: show plugins control menu (true/false)
          ,eventSynch: (Bool) If true, this class initializes an internal event Handler for
                       ActionScript event synchronization
          ,listeners  : {"mouseover": function() {}, .... } DOM listeners to set on the media object each time the Media is rendered.
          ,requiredVersion: (String,Array,Number) If specified, used in version detection.
          ,unsupportedText: (String,DomHelper cfg) Text to render if plugin is not installed/available.
          ,installUrl:(string) Url to inline SWFInstaller, if specified activates inline Express Install.
          ,installRedirect : (string) optional post install redirect
          ,installDescriptor: (Object) optional Install descriptor config
         }
    */

(function(){

   var ux = Ext.ux.Media;

    ux.Flash = Ext.extend( ux, {
        constructor: function() {
            ux.Flash.superclass.constructor.apply(this, arguments);
        },

        SWFObject      : null,

        varsName       :'flashVars',

        hideMode       : 'nosize',

        mediaType: Ext.apply({
              tag      : 'object'
             ,cls      : 'x-media x-media-swf'
             ,type     : 'application/x-shockwave-flash'
             ,loop     : null
             ,scripting: "sameDomain"
             ,start    : true
             ,unsupportedText : {cn:['The Adobe Flash Player{0}is required.',{tag:'br'},{tag:'a',cn:[{tag:'img',src:'http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif'}],href:'http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash',target:'_flash'}]}
             ,params   : {
                  movie     : "@url"
                 ,play      : "@start"
                 ,loop      : "@loop"
                 ,menu      : "@controls"
                 ,quality   : "high"
                 ,bgcolor   : "#FFFFFF"
                 ,wmode     : "opaque"
                 ,allowscriptaccess : "@scripting"
                 ,allowfullscreen : false
                 ,allownetworking : 'all'
                }
             },Ext.isIE?
                    {classid :"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
                     codebase:"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"
                    }:
                    {data     : "@url"}),

        getMediaType: function(){
             return this.mediaType;
        },
        // private (called once by initComponent)
        initMedia : function(){

            var mc = Ext.apply({}, this.mediaCfg||{});
            var requiredVersion = (this.requiredVersion = mc.requiredVersion || this.requiredVersion|| false ) ;
            var hasFlash  = !!(this.playerVersion = this.detectVersion());
            var hasRequired = hasFlash && (requiredVersion?this.assertVersion(requiredVersion):true);

            var unsupportedText = this.assert(mc.unsupportedText || this.unsupportedText || (this.getMediaType()||{}).unsupportedText,null);
            if(unsupportedText){
                 unsupportedText = Ext.DomHelper.markup(unsupportedText);
                 unsupportedText = mc.unsupportedText = String.format(unsupportedText,
                     (requiredVersion?' '+requiredVersion+' ':' '),
                     (this.playerVersion?' '+this.playerVersion+' ':' Not installed.'));
            }

            if(!hasRequired ){
                this.autoMask = false;

                //Version check for the Flash Player that has the ability to start Player Product Install (6.0r65)
                var canInstall = hasFlash && this.assertVersion('6.0.65');

                if(canInstall && mc.installUrl){

                       mc =  mc.installDescriptor || {
                             tag      : 'object'
                            ,cls      : 'x-media x-media-swf x-media-swfinstaller'
                            ,id       : 'SWFInstaller'
                            ,type     : 'application/x-shockwave-flash'
                            ,data     : "@url"
                            ,url              : mc.installUrl
                            //The dimensions of playerProductInstall.swf must be at least 310 x 138 pixels,
                            ,width            : (/%$/.test(mc.width)) ? mc.width : ((parseInt(mc.width,10) || 0) < 310 ? 310 :mc.width)
                            ,height           : (/%$/.test(mc.height))? mc.height :((parseInt(mc.height,10) || 0) < 138 ? 138 :mc.height)
                            ,loop             : false
                            ,start            : true
                            ,unsupportedText  : unsupportedText
                            ,params:{
                                      quality          : "high"
                                     ,movie            : '@url'
                                     ,allowscriptacess : "always"
                                     ,align            : "middle"
                                     ,bgcolor          : "#3A6EA5"
                                     ,pluginspage      : mc.pluginsPage || this.pluginsPage || "http://www.adobe.com/go/getflashplayer"
                                   }
                        };
                        mc.params[this.varsName] = "MMredirectURL="+( mc.installRedirect || window.location)+
                                            "&MMplayerType="+(Ext.isIE?"ActiveX":"Plugin")+
                                            "&MMdoctitle="+(document.title = document.title.slice(0, 47) + " - Flash Player Installation");
                } else {
                    //Let superclass handle with unsupportedText property
                    mc.mediaType=null;
                }
            }

            /*
            *  Sets up a eventSynch between the ActionScript environment
            *  and converts it's events into native Ext events.
            *  When this config option is true, binds an ExternalInterface definition
            *  to the ux.Media.Flash class method Ext.ux.Media.Flash.eventSynch.
            *
            *  The default binding definition pass the following flashVars to the Flash object:
            *
            *  allowedDomain,
            *  elementID (the ID assigned to the DOM <object> )
            *  eventHandler (the globally accessible function name of the handler )
            *     the default implementation expects a call signature in the form:
            *
            *    ExternalInterface.call( 'eventHandler', elementID, eventString )

            *  For additional flexibility, your own eventSynch may be defined to match an existing
            *  ActionScript ExternalInterface definition.
            */

            if(mc.eventSynch){
                mc.params || (mc.params = {});
                var vars = mc.params[this.varsName] || (mc.params[this.varsName] = {});
                if(typeof vars === 'string'){ vars = Ext.urlDecode(vars,true); }
                var eventVars = (mc.eventSynch === true ? {
                         allowedDomain  : vars.allowedDomain || document.location.hostname
                        ,elementID      : mc.id || (mc.id = Ext.id())
                        ,eventHandler   : 'Ext.ux.Media.Flash.eventSynch'
                        }: mc.eventSynch );

                Ext.apply(mc.params,{
                     allowscriptaccess  : 'always'
                })[this.varsName] = Ext.applyIf(vars,eventVars);
            }

            delete mc.requiredVersion;
            delete mc.installUrl;
            delete mc.installRedirect;
            delete mc.installDescriptor;
            delete mc.eventSynch;


            mc.mediaType = "SWF";
            this.mediaCfg = mc;

            if(this.events){
                this.addEvents(
                     /**
                     * @event flashinit
                     * Fires when the Flash Object reports an initialized state via a public callback function.
                     * this callback must implemented to be useful.  Example:
                     *
                     * //YouTube Global ready handler
                       var onYouTubePlayerReady = function(playerId) {

                           //Search for a ux.Flash-managed player.
                           var flashComp, el = Ext.get(playerId);
                           if(flashComp = (el?el.ownerCt:null)){
                              flashComp.onFlashInit();
                           }

                       };
                     *
                     * @param {Ext.ux.Flash} this
                     * @param {Element} the SWFObject interface
                     */
                    'flashinit',

                    /**
                     * @event fscommand
                     * Fires when the Flash Object issues an fscommand to the ux.Flash Component
                     * @param {Ext.ux.Flash} this
                     * @param command {string} the command string
                     * @param args {string} the arguments string
                     */
                    'fscommand'
               );
            }
            ux.Flash.superclass.initMedia.call(this);

        }


        /*
        * Asserts the desired version against the installed Flash Object version.
        * Acceptable parameter formats for versionMap:
        *
        *  '9.0.40' (string)
        *   9  or 9.1  (number)
        *   [9,0,43]  (array)
        *
        *  Returns true if the desired version is => installed version
        *  and false for all other conditions
        */
        ,assertVersion : function(versionMap){

            var compare;
            versionMap || (versionMap = []);

            if(Ext.isArray(versionMap)){
                compare = versionMap;
            } else {
                compare = String(versionMap).split('.');
            }
            compare = (compare.concat([0,0,0,0])).slice(0,3); //normalize

            var tpv;
            if(!(tpv = this.playerVersion || (this.playerVersion = this.detectVersion()) )){ return false; }

            if (tpv.major > parseFloat(compare[0])) {
                        return true;
            } else if (tpv.major == parseFloat(compare[0])) {
                   if (tpv.minor > parseFloat(compare[1]))
                            {return true;}
                   else if (tpv.minor == parseFloat(compare[1])) {
                        if (tpv.rev >= parseFloat(compare[2])) { return true;}
                        }
                   }
            return false;
        },
        /*
        * Flash version detection function
        * Modifed version of the detection strategy of SWFObject library : http://blog.deconcept.com/swfobject/
        * returns a {major,minor,rev} version object or
        * false if Flash is not installed or detection failed.
        */
        detectVersion : function(){
            if(this.mediaVersion){
                return this.mediaVersion;
            }
            var version=false;
            var formatVersion = function(version){
              return version && !!version.length?
                {major:version[0] !== null? parseInt(version[0],10): 0
                ,minor:version[1] !== null? parseInt(version[1],10): 0
                ,rev  :version[2] !== null? parseInt(version[2],10): 0
                ,toString : function(){return this.major+'.'+this.minor+'.'+this.rev;}
                }:false;
            };
            var sfo= null;
            if(Ext.isIE){

                try{
                    sfo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
                }catch(e){
                    try {
                        sfo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
                        version = [6,0,21];
                        // error if player version < 6.0.47 (thanks to Michael Williams @ Adobe for this solution)
                        sfo.allowscriptaccess = "always";
                    } catch(ex) {
                        if(version && version[0] === 6)
                            {return formatVersion(version); }
                        }
                    try {
                        sfo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
                    } catch(ex1) {}
                }
                if (sfo) {
                    version = sfo.GetVariable("$version").split(" ")[1].split(",");
                }
             }else if(navigator.plugins && navigator.mimeTypes.length){
                sfo = navigator.plugins["Shockwave Flash"];
                if(sfo && sfo.description) {
                    version = sfo.description.replace(/([a-zA-Z]|\s)+/, "").replace(/(\s+r|\s+b[0-9]+)/, ".").split(".");
                }
            }
            return (this.mediaVersion = formatVersion(version));

        }
        //Private
        ,_applyFixes : function() {
            var o ;
             // Fix streaming media troubles for IE
             // IE has issues with loose references when removing an <object>
             // before the onload event fires (all <object>s should have readyState == 4 after browsers onload)

             // Advice: do not attempt to remove the Component before onload has fired on IE/Win.

            if(Ext.isIE && Ext.isWindows && (o =this.SWFObject)){
                o.style.display = 'none'; //hide it regardless of state
                if(o.readyState == 4){
                    for (var x in o) {
                        if (typeof o[x] == 'function') {
                            o[x] = null;
                        }
                    }
                }

            }

        }
        ,onAfterMedia : function(ct){

              ux.Flash.superclass.onAfterMedia.apply(this,arguments);
              this.SWFObject = this.getInterface();
              if(this.mediaObject){
                  var id = this.mediaObject.id;
                  if(Ext.isIE ){

                    //fscommand bindings
                    //implement a fsCommand event interface since its not supported on IE when writing innerHTML

                    if(!(Ext.query('script[for='+id+']').length)){
                      writeScript('var c;if(c=Ext.getCmp("'+this.id+'")){c.onfsCommand.apply(c,arguments);}',
                                  {event:"FSCommand", htmlFor:id});
                    }
                  }else{
                      window[id+'_DoFSCommand'] || (window[id+'_DoFSCommand']= this.onfsCommand.createDelegate(this));

                  }

              }

         },
        //Remove (safely) an existing mediaObject from the Component.
        clearMedia  : function(){

           //de-register fscommand hooks
           if(this.mediaObject){
               var id = this.mediaObject.id;
               if(Ext.isIE){
                    Ext.select('script[for='+id+']',true).remove();
               } else {
                    window[id+'_DoFSCommand']= null;
                    delete window[id+'_DoFSCommand'];
               }
               this._applyFixes();
           }

           ux.Flash.superclass.clearMedia.call(this);
           this.SWFObject = null;
        },

        getSWFObject : function() {
            return this.getInterface();
        },


        //http://www.northcode.com/blog.php/2007/09/11/FSCommand-and-getURL-Bug-in-Flash-Player-9
        onfsCommand : function( command, args){

            if(this.events){
                this.fireEvent('fscommand', this, command ,args );
            }

        },

        //Use Flash's SetVariable method if available
        //returns false if the function is not supported.
        setVariable : function(vName, value){
            var fo = this.getInterface();
            if(fo && typeof fo.SetVariable != 'undefined'){
                fo.SetVariable(vName,value);
                return true;
            }
            return false;

        },

        //Use Flash's SetVariable method if available
        //returns false if the function is not supported.
        getVariable : function(vName){
            var fo = this.getInterface();
            if(fo && typeof fo.GetVariable != 'undefined'){
                return fo.GetVariable(vName);
            }
            return undefined;

        },

        /* this function is designed to be used when a player object notifies the browser
         * if its initialization state
         */
        onFlashInit  :  function(){


            if(this.mediaMask && this.autoMask){this.mediaMask.hide();}
            this.fireEvent.defer(10,this,['flashinit',this, this.getInterface()]);

        },
        /**
         * Dispatches events received from the SWF object (when defined by the eventSynch mediaConfig option).
         *
         * @method _handleSWFEvent
         * @private
         */
        _handleSWFEvent: function(event)
        {
            var type = event.type||event||false;
            if(type){
                 if(this.events && !this.events[String(type)])
                     { this.addEvents(String(type));}

                 return this.fireEvent.apply(this, [String(type), this].concat(Array.prototype.slice.call(arguments,0)));
            }
        }

    });
    // Class Method to handle defined Flash interface events
    ux.Flash.eventSynch = function(elementID, event /* additional arguments optional */ ){
            var SWF = Ext.get(elementID);
            if(SWF && SWF.ownerCt){
                return SWF.ownerCt._handleSWFEvent.apply(SWF.ownerCt, Array.prototype.slice.call(arguments,1));
            }
        };

    /* Generic Flash BoxComponent */
   Ext.ux.FlashComponent = Ext.extend(Ext.ux.MediaComponent,{
           ctype : "Ext.ux.FlashComponent",
           getId : function(){
                 return this.id || (this.id = "flash-comp" + (++Ext.Component.AUTO_ID));
           }

   });

   //Inherit the Media.Flash class interface
   Ext.apply(Ext.ux.FlashComponent.prototype, ux.Flash.prototype);
   Ext.reg('uxflash', Ext.ux.FlashComponent);

   ux.Panel.Flash = Ext.extend(ux.Panel,{
          ctype : "Ext.ux.Media.Panel.Flash"
   });

   Ext.apply(ux.Panel.Flash.prototype, ux.Flash.prototype);

   Ext.reg('flashpanel', Ext.ux.MediaPanel.Flash = Ext.ux.FlashPanel = ux.Panel.Flash);
   Ext.reg('uxflashpanel', ux.Panel.Flash);

   Ext.ux.FlashWindow = (ux.Window.Flash = Ext.extend(ux.Window, {ctype : "Ext.ux.FlashWindow", animCollpase:true}));
   Ext.apply(ux.Window.Flash.prototype, ux.Flash.prototype);

   var writeScript = function(block, attributes) {
        attributes = Ext.apply({},attributes||{},{type :"text/javascript",text:block});

         try{
            var head,script, doc= document;
            if(doc && doc.getElementsByTagName){
                if(!(head = doc.getElementsByTagName("head")[0] )){

                    head =doc.createElement("head");
                    doc.getElementsByTagName("html")[0].appendChild(head);
                }
                if(head && (script = doc.createElement("script"))){
                    for(var attrib in attributes){
                          if(attributes.hasOwnProperty(attrib) && attrib in script){
                              script[attrib] = attributes[attrib];
                          }
                    }
                    return !!head.appendChild(script);
                }
            }
         }catch(ex){}
         return false;
    }
})();

/*
 * Ext JS Library 2.2.1
 * Copyright(c) 2006-2009, Ext JS, LLC.
 * licensing@extjs.com
 * 
 * http://extjs.com/license
 */

Ext.grid.RowExpander = function(config){
    Ext.apply(this, config);

    this.addEvents({
        beforeexpand : true,
        expand: true,
        beforecollapse: true,
        collapse: true
    });

    Ext.grid.RowExpander.superclass.constructor.call(this);

    if(this.tpl){
        if(typeof this.tpl == 'string'){
            this.tpl = new Ext.Template(this.tpl);
        }
        this.tpl.compile();
    }

    this.state = {};
    this.bodyContent = {};
};

Ext.extend(Ext.grid.RowExpander, Ext.util.Observable, {
    header: "",
    width: 20,
    sortable: false,
    fixed:true,
    menuDisabled:true,
    dataIndex: '',
    id: 'expander',
    lazyRender : true,
    enableCaching: true,

    getRowClass : function(record, rowIndex, p, ds){
        p.cols = p.cols-1;
        var content = this.bodyContent[record.id];
        if(!content && !this.lazyRender){
            content = this.getBodyContent(record, rowIndex);
        }
        if(content){
            p.body = content;
        }
        return this.state[record.id] ? 'x-grid3-row-expanded' : 'x-grid3-row-collapsed';
    },

    init : function(grid){
        this.grid = grid;

        var view = grid.getView();
        view.getRowClass = this.getRowClass.createDelegate(this);

        view.enableRowBody = true;

        grid.on('render', function(){
            view.mainBody.on('mousedown', this.onMouseDown, this);
        }, this);
    },

    getBodyContent : function(record, index){
        if(!this.enableCaching){
            return this.tpl.apply(record.data);
        }
        var content = this.bodyContent[record.id];
        if(!content){
            content = this.tpl.apply(record.data);
            this.bodyContent[record.id] = content;
        }
        return content;
    },

    onMouseDown : function(e, t){
        if(t.className == 'x-grid3-row-expander'){
            e.stopEvent();
            var row = e.getTarget('.x-grid3-row');
            this.toggleRow(row);
        }
    },

    renderer : function(v, p, record){
        p.cellAttr = 'rowspan="2"';
        return '<div class="x-grid3-row-expander">&#160;</div>';
    },

    beforeExpand : function(record, body, rowIndex){
        if(this.fireEvent('beforeexpand', this, record, body, rowIndex) !== false){
            if(this.tpl && this.lazyRender){
                body.innerHTML = this.getBodyContent(record, rowIndex);
            }
            return true;
        }else{
            return false;
        }
    },

    toggleRow : function(row){
        if(typeof row == 'number'){
            row = this.grid.view.getRow(row);
        }
        this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'expandRow' : 'collapseRow'](row);
    },

    expandRow : function(row){
        if (!row) return;
        if(typeof row == 'number'){
            row = this.grid.view.getRow(row);
        }
        var record = this.grid.store.getAt(row.rowIndex);
        var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
        if(this.beforeExpand(record, body, row.rowIndex)){
            this.state[record.id] = true;
            Ext.fly(row).replaceClass('x-grid3-row-collapsed', 'x-grid3-row-expanded');
            this.fireEvent('expand', this, record, body, row.rowIndex);
        }
    },

    collapseRow : function(row){
        if(typeof row == 'number'){
            row = this.grid.view.getRow(row);
        }
        var record = this.grid.store.getAt(row.rowIndex);
        var body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
        if(this.fireEvent('beforecollapse', this, record, body, row.rowIndex) !== false){
            this.state[record.id] = false;
            Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed');
            this.fireEvent('collapse', this, record, body, row.rowIndex);
        }
    }
});

Ext.override(Ext.grid.RowExpander, {
	/**
	 * @property expandedRows
	 * @type Object
	 * An array of all currently expanded rows.
	 */
	expandedRows: {},
	
	init: function(grid) {
		this.grid = grid;

		var view = grid.getView();
		view.getRowClass = this.getRowClass.createDelegate(this);

		view.enableRowBody = true;

		grid.on('render', function() {
	    view.mainBody.on('mousedown', this.onMouseDown, this);
			
			// Listen to the store onLoad event
			var store = grid.getStore();
			store.on('load', this.restore, this);
		}, this);
  },

	renderer: function(v, p, record) {
		// If the user has no permission, and desc and tags are empty, dont expand
		if (!record.get('description') && !record.get('tags') && !record.get('can_edit')) return;
		
    p.cellAttr = 'rowspan="2"';
    return '<div class="x-grid3-row-expander">&#160;</div>';
  },
	
	expandRow: function(row) {
	  if (!row) return;
	  
    if(typeof row == 'number' || typeof row == 'string') row = this.grid.view.getRow(row);
		
    var record = this.grid.store.getAt(row.rowIndex);
    var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
		
		// If the user has no permission, and desc and tags are empty, dont expand
		if (!record.get('description') && !record.get('tags') && !record.get('can_edit')) return;
		
    if (this.beforeExpand(record, body, row.rowIndex)) {
      this.state[record.id] = true;
			this.expandedRows[row.rowIndex] = row;
      Ext.fly(row).replaceClass('x-grid3-row-collapsed', 'x-grid3-row-expanded');
      this.fireEvent('expand', this, record, body, row.rowIndex);
    };
  },

  collapseRow: function(row) {
    if (!row) return;
    
    if(typeof row == 'number') row = this.grid.view.getRow(row);
		
    var record = this.grid.store.getAt(row.rowIndex);
    var body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
		
    if (this.fireEvent('beforecollapse', this, record, body, row.rowIndex) !== false){
      this.state[record.id] = false;
			this.expandedRows[row.rowIndex] = null;
      Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed');
      this.fireEvent('collapse', this, record, body, row.rowIndex);
    };
  },

	/**
	 * Method which restores all the expandable states of each of the rows in the grid
	 * if any have been expanded
	 */
	restore: function() {
		// Loop through each of the rows
		for (row in this.expandedRows) {
			if (this.expandedRows[row] == null) return;
			this.expandRow(row);
		};
	}
});

Ext.ns('Kohive', 'Kohive.app', 'Kohive.models', 'Kohive.controllers', 'Kohive.views', 'Kohive.exception', 'Kohive.QuickApp', 'Kohive.ux');

/*
 * Kohive.OS
 * @extends ExtMVC.OS
 * Specialisation of ExtMVC.OS, creates and manages various Kohive OS elements
 */
Kohive.OS = function(config) {
  var config = config || {};
  
  //super (ExtMVC.OS)  
  Kohive.OS.superclass.constructor.call(this, config);
	
  /**
   * @property ircManager
   * @type Ext.ux.IRC.Manager
   * A global IRC manager instance 
   */
  this.ircManager = new Ext.ux.IRC.Manager({
    autoConnect: false,
    server: {
      serverHost:      'irc.kohive.com',
			serverPort:       8667,
			serverPolicyPort: 16666,
      password:        'secret123',
      proxySwfPath:    '/IrcProxy.swf',
      showLogMessages: false
    }
  });
  
  /**
   * FIXME: No no no this shouldn't be here.  It should be in a controller but Ext MVC
   * is too shit :(.  Wait for v0.6
   */
  this.on('membersload', function() {
    var manager = Kohive.OS.ircManager;
    
    if (!manager.proxy.connected) {
      var idCard = this.getIdCardData();
      var userName = String.format("KohiveUser--{0}", this.currentUser.get('id'));
      Ext.apply(manager.proxy, {
        nickname: userName,
        realname: userName
      });
      
      manager.proxy.connect();
    }
  }, this);
  
  this.on('launch', this.loadSession, this);
};

Ext.extend(Kohive.OS, ExtMVC.OS, {
  usesHistory:           true,
  dispatchHistoryOnLoad: false,
  viewportBuilder:       'kohive',
  
  /**
   * Maintains a reference to the currently loaded Hive
   */
  currentHive: null,
  
  /**
   * Maintains a reference to the currently logged in user
   */
  currentUser: null,
  
  /**
   * Maintains a reference to the current user membership
   */
  currentMemberships: null,
  
  /**
   * Maintains a reference to the current hives memberships
   */
  currentHiveMemberships: null,
  
  /**
   * @property initialized
   * @type Boolean
   * Tracks the initialization status.  This is set to true after onLaunch() is run. Private
   */
  initialized: false,
  
  /**
   * Initializes the Kohive application, sets up Viewport and other OS objects
   * Call this with Ext.onReady
   * @return {Boolean} True if initialization was successful or had already occured (test this independently with isInitialized())
   */
  onLaunch: function() {
    if (!this.initialized) {
      // Initialise the hives and members controllers, attaches events
      // this.hives   = this.getController('hives');
      // this.members = this.getController('members');
      var os      = this;
      
      if (!this.isDevelopment()) {
        this.startRunners();
      } else {
        var hives   = this.getController('hives');
        var members = this.getController('members');
      }
      
      // Add the background container
      this.bgImageEl = Ext.getBody().createChild({
        tag: 'img',
        cls: 'background-image',
        // src: Kohive.url('resources', 'images/backgrounds/wallpaper1.jpg'),
        id:  'kohive-background-image'
      });
      
      this.addStickiesWrapperToDesktop();
      
      // Resize the background on window resize
      var obj = this;
      $(window).bind('resize', function() {
        obj.bgImageEl.setWidth(Ext.getBody().getWidth());
        obj.bgImageEl.setHeight(Ext.getBody().getHeight());
      });
      
      // Start sending pings to the server so it knows when the user is online
      // this.initLastSeenOnline();
			
			//preload images on the site
			this.preloadImages();
      
      // Add an event to listen to hiveload so we can update the hive when done
      this.on('hiveload', this.updateHiveBackground, this);
      this.on('membersload', this.initHistory, this);
      // this.on('user-loaded', this.checkForFirstLogin, this);
			
      this.initialized = true;
    };
    
    return this.initialized;
  },
  
	/**
	 * Checks if we are viewing the site on kohive.com, if so, return false
	 */
	isDevelopment: function() {
	  var regexp = new RegExp('kohive.com');
	  // Can be turned on in development mode now, too, since the ping request isn't that annoying anymore
    return false; //!regexp.test(location.host);
	},
	
	loadMembers: function() {
    var hives   = this.getController('hives');
    var members = this.getController('members');
    
    if (!this.getCurrentHive()) return;
    hives.fireAction('refresh', this);
    members.fireAction('loadMembers', this, [this.getMemberPanel(), null, false]);
  },
	
	startRunners: function() {
    // Initialise the hives and members controllers, attaches events
    var hives   = this.getController('hives');
    var members = this.getController('members');
    var os      = this;
    
		this.loopingMode = true;
		
    // this.loadMembersTask = {
    //       run: function() {
    //         if (!this.getCurrentHive()) return;
    //         hives.fireAction('refresh', this);
    //         members.fireAction('loadMembers', this, [this.getMemberPanel(), null, false]);
    //       },
    //       scope:    this,
    //       interval: 25000 // 15 seconds
    //     };
    
		this.checkUpdatesTask = {
			run: function() {
        // if (Kohive.OS.membersLoaded && Kohive.OS.applicationsLoaded) {
				  this.checkForUpdates();
        // }
			},
			scope:    this,
			interval: 5000 // 5 seconds
		};
		
		this.checkUpdatesTaskRunner = new Ext.util.TaskRunner();
    this.checkUpdatesTaskRunner.start(this.checkUpdatesTask);
		
    // this.loadMembersTaskRunner = new Ext.util.TaskRunner();
    // this.loadMembersTaskRunner.start(this.loadMembersTask);
	},
	
	stopRunners: function() {
    // // Initialise the hives and members controllers, attaches events
    // var hives   = this.getController('hives');
    // var members = this.getController('members');
    //    
		this.loopingMode = false;
		
    if (this.checkUpdatesTaskRunner)
      this.checkUpdatesTaskRunner.stopAll();
      
    // if (this.loadMembersTaskRunner)
    //   this.loadMembersTaskRunner.stopAll();
	},
	
	/**
	 * Enables the loop to update the site
	 */
	enableLoops: function() {
		this.productionMode();
	},
	
  // /**
  //  * Enables the loop to update the site
  //  */
  // disableLoops: function() {
  //  this.developmentMode();
  // },
	
  initHistory: function() {
    Kohive.OS.handleHistoryChange(window.document.location.hash);
    
    this.un('membersload', this.initHistory, this);
  },
	
	pinging: false,
	
	/**
	 * Pings the server and checks if there are any updates. If there is, it then
	 * fires off the proper actions/events
	 */
	checkForUpdates: function() {
		if (this.fireEvent('before-ping') !== false) {
			if (!this.getCurrentHive()) return;
			if (this.pinging === true) return;
			
			//TODO move this to the proper way below
			//reload the applications
      // this.getApplicationLauncher().populateFromHive(true);
			
			this.pinging = true;
			
			//check for updates
			var updates = Kohive.models.Update.findAll({
				url:   this.scopedUrl('ping'),
				baseParams: {
				  hive_id: this.getCurrentHiveId()
				},
				listeners: {
					scope  : this,
					'loadexception' : function() {
						Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
						this.pinging = false;
					},
					'load' : function(store) {
					  this.pinging = false;
						//if the store count is over 0, then show the message
						if (store.getTotalCount() > 0) {
							store.each(function(record) {
							  if (record.get('part') == "messages") {
							    this.getController('members').fireAction('loadMessages');
							  } else if (["members", "member-went-online", "member-went-offline", "memberships", "applications", "hives"].hasObject(record.get('part'))) {
							    this.loadMembers();
							    this.getApplicationLauncher().populateFromHive(true);
						      Kohive.OS.desktopStickiesDataview.store.reload();
						    } else if (record.get('part') == 'desktop-stickies') {
						      Kohive.OS.desktopStickiesDataview.store.reload();
					      }
							}, this);
						} else {
							Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
						};
					}
				}
			});
		};
	},
  
	/**
	 * Returns false for events
	 */
	returnFalse: function() {
		return false;
	},
	
  /**
   * 
   */
  updateHiveBackground: function(hive) {
    if (!hive) return;
    
    // Get the hive data
    var hive = hive.data.hive;
    
    // Update the background colour/image
    this.setBackgroundColour(hive.background_image || null);
    
    this.updateDesktopStickies();
  },
  
  getBackgroundColours: function() {
    if (!this.defaultBackgroundColours) {
      this.defaultBackgroundColours = new Ext.data.SimpleStore({
        fields: ['name', 'image', 'thumb', 'hex'],
        data: [
          ['blue', '#0C6B9C', '/images/wallpapers/thumbs/0C6B9C.jpg'],
          ['pink', '#DED8F2', '/images/wallpapers/thumbs/d577cf.jpg'],
          ['grey', '#D8D8D9', '/images/wallpapers/thumbs/bbbbbb.jpg'],
          ['lightblue', '#D2E0EF', '/images/wallpapers/thumbs/8cc001.jpg'],
          ['greyblue', '#768294', '/images/wallpapers/thumbs/dc9703.jpg'],
          ['darkblue', '#002841', '/images/wallpapers/thumbs/dark.jpg', '#002841'],
          ['white', '#EFEFEF', '/images/wallpapers/thumbs/white.jpg', '#EFEFEF'],
          ['black', '#000000', '/images/wallpapers/thumbs/black.jpg', '#000000'],
          ['1', '/images/wallpapers/1.jpg', '/images/wallpapers/thumbs/1.jpg', '#0C6B9C'],
          ['3', '/images/wallpapers/3.jpg', '/images/wallpapers/thumbs/3.jpg', '#0C6B9C'],
          ['15', '/images/wallpapers/15.jpg', '/images/wallpapers/thumbs/15.jpg', '#0C6B9C'],
          ['2', '/images/wallpapers/2.jpg', '/images/wallpapers/thumbs/2.jpg', '#0C6B9C'],
          ['6', '/images/wallpapers/6.jpg', '/images/wallpapers/thumbs/6.jpg', '#0C6B9C'],
          ['12', '/images/wallpapers/12.jpg', '/images/wallpapers/thumbs/12.jpg', '#0C6B9C'],
          ['10', '/images/wallpapers/10.jpg', '/images/wallpapers/thumbs/10.jpg', '#0C6B9C'],
          ['11', '/images/wallpapers/11.jpg', '/images/wallpapers/thumbs/11.jpg', '#0C6B9C'],
          ['13', '/images/wallpapers/13.jpg', '/images/wallpapers/thumbs/13.jpg', '#0C6B9C'],
          ['4', '/images/wallpapers/4.jpg', '/images/wallpapers/thumbs/4.jpg', '#0C6B9C'],
          ['5', '/images/wallpapers/5.jpg', '/images/wallpapers/thumbs/5.jpg', '#0C6B9C'],
          ['14', '/images/wallpapers/14.jpg', '/images/wallpapers/thumbs/14.jpg', '#0C6B9C'],
          ['16', '/images/wallpapers/16.jpg', '/images/wallpapers/thumbs/16.jpg', '#0C6B9C']
        ]
      });
    };
    
    return this.defaultBackgroundColours;
  },
  
  /**
   * Sets the background colour of the site. If none is set then it reverts to the original (blue)
   * @param {String/Integer} colour The colour/Url for the background
   * @param {Object} save True if you want to save it to the server. Defaults to false
   */
  setBackgroundColour: function(colour, save) {
    this.bgImageEl.dom.src = "";
    this.bgImageEl.hide();
    Ext.getBody().applyStyles('background:#0C6B9C');
    
    if (!colour) { return; };
    
    var colourOrginal = colour;
    colour = (typeof colour == "object") ? colour.image : colour;
    
    // Check if we need to save it
    // Save it if specified
    if (save) {
      var hive = new Kohive.models.Hive(this.getCurrentHive().data.hive);
      hive.set('background_image', colour);
      
      var config = (typeof save == "object") ? save : {};
      
      Ext.applyIf(config, {
        url: '/hives/' + this.getCurrentHive().data.hive.id
      });
      
      // Save the hive
      hive.save(config);
    };
    
    // Check if it is a URL
    var isUrl = false;
    for (var i = 0; i < colour.length; i++) {
      if (colour[i] == "/") {
        isUrl = true;
      };
    };
    
    // If it is a Url, update the background image url
    if (isUrl) {
      // Set the source of the picture
      this.bgImageEl.dom.src = colour;
      
      // Set the width and height of the image
      this.bgImageEl.setWidth(Ext.getBody().getWidth());
      this.bgImageEl.setHeight(Ext.getBody().getHeight());
      
      Ext.getBody().applyStyles('background:' + colourOrginal.hex || (this.getBackgroundColours().data.items[this.getBackgroundColours().find('image', colour)] || '#0C6B9C'));
      
      this.bgImageEl.show();
      
      return;
    };
    
    // Check if it is hashed
    var hashed = false;
    for (var i = 0; i < colour.length; i++) {
      if (colour[i] == "#") {
        hashed = true;
      };
    };
    
    if (hashed) {
      Ext.getBody().applyStyles('background:' + colour);
    } else {
      Ext.getBody().applyStyles('background:#' + colour);
    };
  },
  
  /**
   * Attempts to load a hive from the window's location.  Takes everything after the #
   * to be the hive url slug
   */
  loadHiveFromUrl: function() {
    var token = window.location.hash.replace('#', '').split("/")[0];
    
		//check if a token is set and if it exists
    if (!token || this.checkIfHiveExists(token) === false) return this.loadFromDefaultHiveId();
    
		//load the hive
    this.getController('hives').fireAction('loadHive', null, [token]);
  },
  
  /**
   * Attempts to load a hive from the users default_hive_id. If it fails it will load the first hive in the users list
   */
  loadFromDefaultHiveId: function() {
    var user = this.getCurrentUser() || null;
		if (!user) return;
		
		//get the current memberships
    var memberships = this.getCurrentMemberships() || null;
		
    // If the current user has been loaded, get the default hive id
    var defaultHiveId = user.get('default_hive_id') || null;
    
		//check if the default hvie exists
		if (defaultHiveId) {
			var exists = this.checkIfHiveExists(defaultHiveId);
		};
		
    // Check if the current user has been loaded, if not, open the first hive
    if (!user || !defaultHiveId || !memberships.data.items[0] || exists === false) {
      // If the memberships isn't found, the return an error
      if (!memberships) return Ext.Msg.alert('Load hive', '312:' + Ext.ux.Translator.translate('something_wrong'));
      
			if (!memberships.data.items[0]) return;
			
      // Get the first hive id from the first membership
      defaultHiveId = memberships.data.items[0].data.hive_id || null;
    };
    
    // Load the hive
    if (defaultHiveId) this.getController('hives').fireAction('loadHive', null, [defaultHiveId]);
  },
  
	checkIfHiveExists: function(id) {
		var exists = false,
		    memberships = this.getCurrentMemberships() || null;
		
		if (!id) return exists;
		
		memberships.each(function(membership) {
			if (membership.get('hive_id') == id) {
				exists = true;
			};
		}, this);
		
		return exists;
	},
	
  /**
   * Returns an Ext.data.Store containing available colours for the header panel
   * @return {Ext.data.Store} A simple store with allowed colours for the header panel
   */
  getHeaderColours: function() {
    if (!this.defaultHeaderColours) {
      this.defaultHeaderColours = new Ext.data.SimpleStore({
        fields: ['colour', 'hex'],
        data: [
          ['blue',   'blue'],
          ['green',  'green'],
          ['grey',   'grey'],
          ['pink',   'pink'],
          ['yellow', 'yellow'],
          ['purple', 'purple'],
          ['black', 'black'],
          ['red', 'red'],
          ['lightblue', 'blue']
                    
        ]
      });
    };
    
    return this.defaultHeaderColours;
  },
  
  /**
   * Attempts to retrieve session information from the server
   */
  loadSession: function(callback, scope) {
    Kohive.models.User.loadFromSession({
      success: function(user) {
        this.currentUser = user;
        
        // Update the username
        Kohive.OS.getHeaderPanel().updateUsername();
        
        this.fireEvent('user-loaded', this.currentUser);
				
				if (callback && typeof callback == "function") callback.call(scope || this);
      },
      scope: this
    });
  },
  
  /**
   * Returns the viewport created by init()
   * @return {Ext.Viewport} The Ext.Viewport for the Kohive app
   */
  getViewport: function() {
    return this.viewport;
  },
  
  /**
   * Returns the Application Launcher
   * @return {Kohive.ApplicationLauncher} The Application Launcher created on startup
   */
  getApplicationLauncher: function() {
    return this.applicationLauncher;
  },
  
  /**
   * Returns the Quick Apps bar
   * @return {Kohive.QuickAppsBar} The Quick Apps bar created on startup
   */
  getQuickAppsBar: function() {
    return this.quickAppsBar;
  },
  
  /**
   * Returns the global HiveSummary component.
   * @return {Kohive.HiveSummary} The shared global HiveSummary component.  See HiveSummary for usage
   */
  getHiveSummary: function() {
    return this.hiveSummary;
  },
  
  /**
   * Returns the global HiveOptions component.
   * @return {Kohive.HiveOptions} The shared global HiveOptions component.  See HiveOptions for usage
   */
  getHiveOptions: function() {
    return this.hiveOptions;
  },
  
  /**
   * Returns the global Hive Panel component
   * @return {Kohive.HivePanel} The shared global HivePanel component. See HivePanel for usage
   */
  getHivePanel: function() {
    return this.hivePanel;
  },
  
  /**
   * Returns the global MemberSummary component.
   * @return {Kohive.MemberSummary} The shared global MemberSummary component.  See MemberSummary for usage
   */
  getMemberSummary: function() {
    return this.memberSummary;
  },
  
  /**
   * Returns the global MemberOptions component.
   * @return {Kohive.MemberOptions} The shared global MemberOptions component.  See MemberOptions for usage
   */
  getMemberOptions: function() {
    return this.memberOptions;
  },
  
  /**
   * Returns the global MemberPanel component
   * @return {Kohive.MemberPanel} The shared global MemberPanel component. See MemberPanel for usage
   */
  getMemberPanel: function() {
    return this.memberPanel;
  },
  
  /**
   * Returns the global Header Panel component
   * @return {Kohive.HeaderPanel} The shared global HeaderPanel component. See HeaderPanel for usage
   */
  getWidgetBar: function() {
    return this.widgetBar;
  },
  
  /**
   * Returns the global Header Panel component
   * @return {Kohive.HeaderPanel} The shared global HeaderPanel component. See HeaderPanel for usage
   */
  getHeaderPanel: function() {
    return this.headerPanel;
  },
  
  /**
   * Returns the global Window Group object
   * @return {Ext.WindowGroup} The Window Group currently providing management for the whole application
   */
  getWindowManager:function() {
    return this.windowManager;
  },
  
  /**
   * Returns a Kohive.models.User object for the currently logged in User,
   * or null if no User is logged in.
   * @return {Kohive.models.User/Null} The currently logged in user
   */
  getCurrentUser: function() {
    return this.currentUser;
  },
  
  /**
   * Ridiculous method to get the Id Card the current user is using for the currently loaded hive
   * @return {ExtMVC.Model.Base} The Id Card
   */
  getIdCardData: function() {
    var idCard, membership = this.getCurrentHive();
    Ext.each(this.getCurrentHiveMemberships() || [], function(mem) {
      if (mem.get('id') == membership.get('id')) {
        idCard = mem.data.id_card;
        return false;
      }
    }, this);
    
    return idCard;
  },
  
  /**
   * Returns a Kohive.models.Hive object for the currently loaded Hive, or
   * null if no Hive is loaded
   * @return {Kohive.models.Hive/Null} The current loaded hive
   */
  getCurrentHive: function() {
    return this.currentHive;
  },
  
  getCurrentMembership: function() {
    return this.getCurrentHive();
  },
  
  getCurrentMembershipId: function() {
    return this.getCurrentHive().get('id');
  },
  
  /**
   * Returns the current hive id
   * @return {Int/Null} The current hive id
   */
  getCurrentHiveId: function() {
    return Kohive.OS.getCurrentHive().get('hive_id');
  },
  
  /**
   * Updates the current hive with an instance of the current hive
   * @param {Kohive.models.Hive} newHive
   */
  setCurrentHive: function(newHive, fireLoadEvent) {
    if (!this.isDevelopment()) this.stopRunners();
    this.fireEvent('beforehiveload', this.currentHive);
    this.currentHive = newHive;
    
    if (fireLoadEvent !== false) {
      this.fireEvent('hiveload', newHive);
      if (!this.isDevelopment()) this.startRunners.defer(500, this);
    };
  },
  
  /**
   * Returns an array of Kohive.models.Membership objects for the currently loaded Hive, or
   * null if no Hive is loaded
   */
  getCurrentMemberships: function() {
    return this.currentMemberships;
  },
  
  /**
   * Updates the current memberships with an instance of all current memberships
   */
  setCurrentMemberships: function(newMemberships) {
    this.currentMemberships = newMemberships;
  },
  
  /**
   * Returns a Kohive.models.Hive object for the currently loaded Hive, or
   * null if no Hive is loaded
   * @return {Kohive.models.Hive/Null} The current loaded hive
   */
  getCurrentHiveMemberships: function() {
    return this.currentHiveMemberships;
  },
  
  /**
   * Updates the current membership with new memberships
   * @param {Kohive.models.Membership} newMembership
   */
  setCurrentHiveMemberships: function(newMemberships) {
    this.currentHiveMemberships = newMemberships;
    
    this.fireEvent('membersload', newMemberships, true);
  },
  
  /**
   * Returns a membership from an id card id
   * @param {Int} id The id of the id card
   * @param {Function} callback A method which will be called one the id card is loaded
   */
  getIdCardFromId: function(id, callback, scope) {
    if (!id || !callback) { return; };
    
    Kohive.models.IdCard.findById(id, {
			url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/id_cards/' + id,
      success: function(idCard) {
        callback.call(scope || this, idCard);
      },
      failure: function() {
        Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
      }
    });
  },
  
  /**
   * Returns a membership from an membership id
   * @param {Int} id The id of the id card
   */
  getIdCardFromMembershipId: function(id, callback, scope) {
    if (!id || !callback) { return; };
    
    Kohive.models.IdCard.findById(id, {
      url: '/memberships/' + id + '/id_card',
      success: function(idCard) {
        callback.call(scope || this, idCard);
      },
      failure: function() {
        Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
      }
    });
  },
  
  /**
   * Runs a task every 1 minute to update the users last seen online field
   */
  // initLastSeenOnline: function() {
  //   var task = {
  //     run: function() {
  //       this.updateLastSeenOnline();
  //     },
  //     scope:    this,
  //     interval: 60000 // 1 minute
  //   };
  //   var runner = new Ext.util.TaskRunner();
  //   runner.start(task);
  // },
  // 
  // /**
  //  * Pings the server so that other users know that the current user is online
  //  */
  // updateLastSeenOnline: function() {
  //   return Ext.Ajax.request({
  //     url:    '/account/ping',
  //     method: 'POST'
  //   });
  // },
	
	/**
	 * Method which creates an empty hidden div in the body of the site and inserts
	 * certain images to pre-load them
	 */
	preloadImages: function() {
		var images = [
			//window
			'/images/ext/kohive/x-window/body/left.png',
			'/images/ext/kohive/x-window/body/right.png',
			'/images/ext/kohive/x-window/body/bg.png',
			'/images/ext/kohive/x-window/body/left-corner.png',
			'/images/ext/kohive/x-window/body/right-corner.png',
			'/images/ext/kohive/x-window/body/bottom.png',
			
			'/images/ext/kohive/x-window/body/window-header-tl.png',
			'/images/ext/kohive/x-window/header/window-header-bg.png',
			'/images/ext/kohive/x-window/header/window-header-tr.png',
			'/images/ext/kohive/x-window/header/bug.png',
			'/images/ext/kohive/x-window/header/help.png',
			'/images/ext/kohive/x-window/header/maximize.png',
			'/images/ext/kohive/x-window/header/close.png',
			
			//icons
			'/images/apps/success.png',
			'/images/components/Universal/delete.png'
		];
		
		var el = Ext.getBody().createChild();
		el.hide();
		
		Ext.each(images, function(source) {
			el.createChild({
				tag: 'img',
				src: source
			});
		}, this);
	},
  
	/**
	 * Returns the default date on the site
	 */
	getDefaultDateFormat: function() {
		return 'j M y';
	},
	
  /**
   * Converts a Rails date into a valid ExtJS date for the site
   * @param {String} field The field you want to convert to a date
   * @param {Boolean} format True if you want to format the date to the default
   */
  convertToDate: function(field, format) {
    var dateStrings = field.split("T")[0].split("-");
    var day   = parseInt(dateStrings[2], 10),
        month = parseInt(dateStrings[1], 10),
        year  = parseInt(dateStrings[0], 10);
    var timeStrings = field.split("T")[1].split(":");
    var hour = parseInt(timeStrings[0], 10),
        min  = parseInt(timeStrings[1], 10),
        sec  = parseInt(timeStrings[2], 10);
    
    var date = new Date(year, month - 1, day, hour, min, sec);
    
    // 21-Mar-09 @ 5:42pm
    if (format) {      
      // return date.format('j-M-y @ g:ia');
      return date.format((typeof format == "string") ? format : Kohive.OS.getDefaultDateFormat());
    };
    
    return date;
  },
  
  /**
   * Converts a database Rails date into a valid ExtJS date for the site
   * @param {String} field The field you want to convert to a date
   * @param {Boolean} format True if you want to format the date to the default
   */
  convertToDateSpecial: function(field, format) {
    var dateStrings = field.split(" ")[0].split("-");
    var day   = parseInt(dateStrings[2], 10),
        month = parseInt(dateStrings[1], 10),
        year  = parseInt(dateStrings[0], 10);
    var timeStrings = field.split(" ")[1].split(":");
    var hour = parseInt(timeStrings[0], 10),
        min  = parseInt(timeStrings[1], 10),
        sec  = parseInt(timeStrings[2], 10);
    
    var date = new Date(year, month - 1, day, hour, min, sec);
    
    // 21-Mar-09 @ 5:42pm
    if (format) {      
      // return date.format('j-M-y @ g:ia');
      return date.format((typeof format == "string") ? format : Kohive.OS.getDefaultDateFormat());
    };
    
    return date;
  },
	
	/**
	 * Checks if the current user is the author of the specified record
	 * @param {Ext.data.Record} record The record to check
	 */
	isAuthor: function(record) {
		//check if anyone is logged in
		var current_user = this.getCurrentUser() || null;
    if (!current_user) return false;
		
		//check for current hive/membership
		var current_hive = this.getCurrentHive() || null;
    if (!current_hive) return false;
		
		if (!record.author_membership_id) return false;
    if (current_hive.data.id == record.author_membership_id) return true;
		
		return false;
	},
	
	/**
	 * Checks if the current user is the custom of the specified record
	 * @param {Ext.data.Record} record The record to check
	 */
	isCustom: function(record, fieldA, fieldB) {
		//check if anyone is logged in
		var current_user = this.getCurrentUser() || null;
    if (!current_user) return false;
		
		//check for current hive/membership
		var current_hive = this.getCurrentHive() || null;
    if (!current_hive) return false;
		
		if (!record[fieldA]) return false;
    if (current_hive.data[fieldB] == record[fieldA]) return true;
		
		return false;
	},
	
	/**
	 * Checks if the current user is a mod of the current hive
	 * @param {Ext.data.Record} record The record to check
	 */
	isHiveMod: function() {
		//check if anyone is logged in
		var current_user = this.getCurrentUser() || null;
    if (!current_user) return false;
		
		//check for current hive/membership
		var current_hive = this.getCurrentHive() || null;
    if (!current_hive) return false;
		
		if (current_hive.data.access_level > 1) return true;
		
		return false;
	},
	
	/**
	 * Checks if the current user is the owner of the current hive
	 * @param {Ext.data.Record} record The record to check
	 */
	isHiveOwner: function() {
		//check if anyone is logged in
		var current_user = this.getCurrentUser() || null;
    if (!current_user) return false;
		
		//check for current hive/membership
		var current_hive = this.getCurrentHive() || null;
    if (!current_hive) return false;
		
		if (current_hive.data.access_level > 2) return true;
		
		return false;
	},
  
  /**
   * Checks if the current user has permission to edit/destroy a given record
   * @param {Ext.data.Record} record The record which the user wants to destroy.
   *                                 taken from an Ext.data.store
   */
  hasPermission: function(record) {
    var type = 1;
    if (typeof record == "boolean" && record == true) { type = 2; };
    
    // Get the logged in user, if cannot be found return false
    var current_user = this.getCurrentUser() || null;
    if (!current_user) { return false; };
    
    // Check if the logged in user is a moderator or owner of the current hive
    var current_hive = this.getCurrentHive() || null;
    if (!current_hive) { return false; };
    if (current_hive.data.access_level > type) { return true; };
    
    // Get the author_membership_id of the record and check if it is the same as the current user who
    // is logged in
    if (!record || typeof record == "boolean") { return false; };
    if (!record.data) { return false; };
    if (!record.data.author_membership_id) { return false; };
    if (current_hive.data.id == record.data.author_membership_id) { return true; };
    
    return false;
  },
  
  /**
   * Overwrite the defalt sets title function so it doesn't update the document title ever.
   */
  setsTitle: Ext.emptyFn,
  
  handleHistoryChange: function(token) {
    // Get the hive id, controller and action if specified in the window location hash
    var hiveId = (token.split('#')[1]) ? token.split('#')[1] : token.split('#')[0] || null;
		
    if (!hiveId) return;
    
    // Get the current hive
    var hive = this.getCurrentHive() || null;
    
    // Get current hive id and check if it is the same
    if (hive) {
      if (hiveId != hive.data.hive.id) {
        this.getController('hives').fireAction('loadHive', null, [hiveId]);
      };
    } else {
      this.getController('hives').fireAction('loadHive', null, [hiveId]);
    };
    
    // Get the controller + action from the token and try and and match it
    var newToken;
    var controller = token.split('/')[1];
    var action     = token.split('/')[2];
    var id         = token.split('/')[3];
    
    if (controller && action && id)   { newToken = controller + "/" + action + "/" + id; };
    if (controller && action && !id)  { newToken = controller + "/" + action; };
    if (controller && !action && !id) { newToken = controller; };
		
    // Try and find the controller + action, and if we can fire off the dispatch
    var match = this.router.recognise(newToken);
    if (match) {
			//sets document title
	    var title = String.format("Kohive : {0}", this.getCurrentHive().data.hive.title);
	    if (controller != undefined) title += String.format(" : {0}", (match.controller && this.getController(match.controller) && this.getController(match.controller).title) ? this.getController(match.controller).title : controller.titleize());
	    document.title = title;
			
			this.dispatch(match);
		} else {
			//sets document title
	    var title = String.format("Kohive : {0}", this.getCurrentHive().data.hive.title);
	    document.title = title;
		};
  },
  
  /**
   * Debug helper method to log fired events for a specific object
   * @param {Observable} observable Any object that extends Ext.util.Observable
   * @param {String} name Name to identify the log messages in the console 
   */
  logFiredEvents: function(observable, name) {
    Ext.util.Observable.capture(observable, function(eventName, el) {
      console.info(name + ' Event:', eventName, el);
    }, this);
  },
	
	/**
	 * Locally set stores for the memberpanel and application launcher. this is needed
	 * so when both are available when they need to be shown
	 */
	storeA: null,
	storeB: null,
	
	/**
	 * Checks if the members are loaded and the applications have loaded, and if they have
	 * it shows both at the same time
	 */
	showMembersAndApplications: function(memberPanel, applicationLauncher, storeA, storeB) {
	  Kohive.OS.log('showMembersAndApplications', arguments);
		var membersController   = this.getController('members');
		var memberPanel         = memberPanel || this.getMemberPanel();
		var applicationLauncher = applicationLauncher || this.getApplicationLauncher();
		
		// if (storeA) this.storeA = storeA;
		// if (storeB) this.storeB = storeB;
		
		//fade them in
		// if (this.storeA && this.storeB) {
			if (storeA) { memberPanel.setStore(storeA); memberPanel.gogogo.defer(1000, memberPanel); };
			if (storeB) { applicationLauncher.setStore(storeB); applicationLauncher.gogogo.defer(1000, applicationLauncher); };
			
			// Kohive.OS.getController('members').loaded = false;
			// applicationLauncher.loaded = false;
		// };
	},
	
	/**
   * TODO: A hacky method to prepend the /memberships/:membership_id/ segment to the front of an AJAX
   * request url.  This really ought to be handled by the model, so factor it out of here when possible
   * @param {String} url The url to append to the memberships scope
   * @return {String} The fully scoped url
   */
  scopedUrl: function(url) {
    return String.format("/memberships/{0}/{1}", this.getCurrentHive().get('id'), url);
  },
	
	// on: function(a) {
	// 	if (a == "before-ping") console.log('on');
	// 	ExtMVC.OS.superclass.on.apply(this, arguments);
	// },
	// 
	// un: function(a) {
	// 	if (a == "before-ping") console.log('un');
	// 	ExtMVC.OS.superclass.un.apply(this, arguments);
	// }
	log: function() {
	  if (this.logging)
	    console.log.apply(this, arguments);
  },
  
  /**
   * Adds the desktop stickies wrapper to the desktop so they can be added/deleted accordingly.
   */
  addStickiesWrapperToDesktop: function() {
    // Add the desktop stickies container
    this.desktopStickiesEl = Ext.getBody().createChild({
      cls: 'desktop-stickies',
      id:  'kohive-desktop-stickies'
    });
    
    var os = this;
    
    this.stickiesDropTarget = new Ext.dd.DropTarget(Ext.getBody(), {
      ddGroup: 'desktop-stickies',
      notifyOver: function(source, e, data) {
        if (os.stickyDropAllowed(data.sticky, e)) {
          return Ext.dd.DropTarget.prototype.dropAllowed;
        } else {
          return Ext.dd.DropTarget.prototype.dropNotAllowed;
        };
      },
      
      notifyDrop: function(source, e, data) {
        if (os.stickyDropAllowed(data.sticky, e)) {
          os.onValidStickyDrop(source, e, data);
          return true;
          // return stack.fireEvent('carddropped', stack, data.card);
        } else {
          return false;
        }
      }
    });
    
    this.desktopStickiesDataview = new Ext.DataView({
      renderTo:     'kohive-desktop-stickies',
      emptyText:    '',
      itemSelector: '.x-pod',
      overClass:    'x-over',
      tpl:          Kohive.models.DesktopSticky.dataView,
      store:        Kohive.models.DesktopSticky.findAll({
        autoLoad: false
      }),
      listeners: {
        scope       : this,
        // 'mouseover' : this.viewOnMouseOver,
        // 'mouseout'  : this.viewOnMouseOut,
        'click'     : function(dataview, index, node, e) {
          var el     = Ext.get(node),
      	      target = Ext.get(e.getTarget()),
      	      record = dataview.getRecord(node);
      	  
      	      //Delete
          	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) {
                var contentEl = el.child('.x-content-wrap') || el;

                return Ext.Msg.show({
                  title:   '',
                  msg:     Ext.ux.Translator.translate('app_desktop_stickies_destroy_desc'),
                  buttons: {yes: Ext.ux.Translator.translate('remove'), no: Ext.ux.Translator.translate('keep')},
                  scope:   this,
                  fn:      function(btn) {
                    if (btn == "yes") Kohive.OS.onDestroyDesktopSticky(record, dataview.store, contentEl);
                  }
                });
          	  };
        }
      }
    });
    
    // Resize the background on window resize
    var obj = this;
    $(window).bind('resize', function() {
      obj.desktopStickiesEl.setWidth(Ext.getBody().getWidth());
      obj.desktopStickiesEl.setHeight(Ext.getBody().getHeight() - 40);
    });
    obj.desktopStickiesEl.setWidth(Ext.getBody().getWidth());
    obj.desktopStickiesEl.setHeight(Ext.getBody().getHeight() - 40);
  },
  
  /**
   * 
   */
  onValidStickyDrop: function(source, e, data) {
    var dragEl = Ext.get(source.getDragEl()),
        el     = Ext.get(source.getEl()).parent(),
        sticky = data.sticky,
        view   = data.view,
        store  = Kohive.OS.desktopStickiesDataview.store;
    
    //remove the drag el
    // dragEl.remove();
    
    //create a new desktop sticky record
    var record = new Kohive.models.DesktopSticky({
      hive_id:              Kohive.OS.getCurrentHiveId(),
      sticky_id:            sticky.get('id'),
      author_membership_id: Kohive.OS.getCurrentHive().get('id')
    });
    
    //show saving on the index view
    view.showLoading(Ext.ux.Translator.translate('app_statusbar_saving_to_desktop'));
    
    //save the record
    record.save({
      scope: this,
      url:   Kohive.OS.scopedUrl('desktop_stickies'),
      success: function() {
        view.showSuccess();
        store.load();
      },
      failure: function(record, response) {
        view.showFailure();
        
        //decode the response
        var response = Ext.decode(response.responseText);
        
        //check what the error is and show it in a ext.msg
        if (response && response.errors && response.errors.length > 0) Ext.Msg.alert('', Ext.ux.Translator.translate(response.errors[0]));
      }
    });
  },
  
  /**
   * 
   */
  stickyDropAllowed: function(sticky, e) {
    var store     = Kohive.OS.desktopStickiesDataview.store,
        duplicate = false;
    
    store.each(function(record) {
      if (record.get('sticky_id') == sticky.data.id) duplicate = true;
    }, this);
    
    if (duplicate === true) return false;
    else return true;
  },
  
  /**
   * Updates the stickies on the desktop
   */
  updateDesktopStickies: function() {
    this.desktopStickiesDataview.el.fadeOut({
      scope: this,
      callback: function() {
        this.desktopStickiesDataview.store.proxy.conn.url = this.scopedUrl('desktop_stickies');
        this.desktopStickiesDataview.store.load({
          scope: this,
          callback: function() {
            this.desktopStickiesDataview.el.fadeIn();
          }
        });
      }
    });
  },
  
  onDestroyDesktopSticky: function(record, store, contentEl) {
    var id = record.get('id');
    
    var u = new Kohive.models.DesktopSticky({id: id});
    u.destroy({
      url:     Kohive.OS.scopedUrl('desktop_stickies/' + record.get('id')),
      scope:   this,
      success: Kohive.OS.onDestroyDesktopStickySuccess.createDelegate(Kohive.OS, [store, contentEl]),
      failure: Kohive.OS.onDestroyDesktopStickyFailure
    });
  },
  
  onDestroyDesktopStickySuccess: function(store, contentEl) {
		contentEl.fadeOut({
			duration: 0.5,
			scope:    this,
			callback: function() {
				store.reload();
			}
		});
  },
  
  onDestroyDesktopStickyFailure: function() {
    Ext.Msg.alert(
      'Delete Failed',
      Ext.ux.Translator.translate('something_wrong')
    );
  },
  
  /**
   * Checks this.currentUser for the 'shown_first_login' param and fires off an event
   * if it is a new user
   */
  checkForFirstLogin: function(user) {
    var user = user || Kohive.OS.getCurrentUser();
    if (!user.get('shown_first_login')) this.fireEvent('show-first-login', user);
  }
});

Kohive.OS = new Kohive.OS();

Ext.ns('Kohive.views.os');

/**
 * Ext.ux.Translator
 * Translation Singleton which translates any string from the base language (UK English by default),
 * to any other language.  Usage:
 * Ext.ux.Translator.translate('fr', "My test string"); // returns whatever the french for that is...
 *
 * We can also set a default translate language, which will then be used as the language to translate
 * to if we only supply one argument to .translate:
 * Ext.ux.Translator.setTranslateLanguage('fr');
 * Ext.ux.Translator.translate("My test string"); // returns the same as above
 */
Ext.ux.Translator = function() {
  
  /**
   * Maintains a hash of all languages and their ISO codes
   */
  var languages = {
    'en-GB': {
      name:         'UK English',
      translations: {}
    }
  };
  
  /**
   * The default language the Translator will use, if no other language
   * has been specified.
   */
  var defaultLanguage = user_language || 'en-GB';
  
  /**
   * Object which will be populated with any language loading errors
   */
  var translationErrors = { 
    languageErrors: {}
  };
  
  return {
    
    /**
     * Returns the translation errors
     * @return {Object} Translation errors
     */
    getErrors: function() {
      return translationErrors;
    },
    
    /**
    * Returns the current default language
    */
    getDefaultLanguage: function(){
      return defaultLanguage;
    },
    
    /**
     * Returns the loaded languages
     */
    getLanguages: function() {
      return languages;
    },
    
    /**
     * Translates the given string into the language identified by the ISO code.
     * Always translates an English string into a foreign language string.
     * This method also accepts a single argument (the string to be translated), so long
     * as setTranslateLanguage() has already been called (see {@see Ext.ux.Translator.setTranslateLanguage})
     * @param {String} key The key of the string to translate from English to another language
     * @param {Object} args Arguments to be passed to an XTemplate
     * @param {Boolean} error Set true if you want to return a boolean if not found
     * @return {String} The translated string
     */
    translate: function(key, args, isoCode, error) {
      var isoCode  = isoCode || defaultLanguage;
      var language = languages[isoCode];
      var args     = args    || {};
      
      if (!key) { return; } //nothing to translate...
      
      // if either the language or the key is not available. if this happens, just return the key
      // surrounded by square brackets
      if (!language || !languages[isoCode].translations[key]) {
        // Check if the default isoCode has been entered into the translation errors object
        if (!translationErrors.languageErrors[isoCode]) {
          translationErrors.languageErrors[isoCode] = [];
        };
        
        // Push the key intos the language errors array
        translationErrors.languageErrors[isoCode].push(key);
        
        if (error === true) return false;
        else return "[" + key + "]";
      };
      
      // Get the translation
      var translation = languages[isoCode].translations[key];
      
      switch(typeof translation) {
        case 'string': return translation;          
        case 'object': return translation.apply(args);
      }
    },
    
    /**
     * Registers a language with its ISO code, full name and optional translations
     * @param {String} isoCode The ISO code for this language
     * @param {String} languageName The full name for this language (should be in language itself (e.g. Francais not French))
     * @param {Object} translations An optional object of key:value pairs for this language
     */
    registerLanguage: function(isoCode, languageName, translations) {
      var translations = translations || {};
      
      //check if the language is already defined. if it is already defined we
      //do not need to add the iso code
      if (!languages[isoCode]) {
        languages[isoCode] = {
          name:         languageName,
          translations: {}
        };
      }
      
      //add the translations for the specified iso code
      Ext.ux.Translator.addTranslationsForLanguage(isoCode, translations);
    },
    
    /**
     * Changes the default language from en-GB to the specified isoCode.
     * @param {String} isoCode The ISO code for this language
     */
    setTranslateLanguage: function(isoCode) {
      if (isoCode) {
        //check if the isoCode is available
        if (languages[isoCode]) {          
          defaultLanguage = isoCode;
        } else {
          return false;
        }
      } else {
        return false;
      }
    },
    
    /**
     * Adds extra translations for an already defined language
     * @param {String} isoCode The ISO code for this language
     * @param {Object} translations An object of key:value pairs for this language
     * @param {Boolean} overwrite If true, ALL old translations will be overwritten. if false
     *                            the old translations which do not have NEW values will be
     *                            kept.
     */
    addTranslationsForLanguage: function(isoCode, translations, overwrite) {
      //check if both the isoCode and translations object are defined
      if (isoCode && translations) {
        //we need to check if all the english keys has been defined in every other
        //registered language
        if(isoCode != 'en-GB'){
          var errors = [];
          
          //loop through each of the english translation keys and check if they are
          //in the new language
          for (key in languages['en-GB'].translations) {
            if(!translations[key]){
              errors.push(key);
            }
          };
          
          // Check if any errors for this isoCode already exist
          if (!translationErrors.languageErrors[isoCode]) {
            translationErrors.languageErrors[isoCode] = [];
          };
          
          // Add the errors
          translationErrors.languageErrors[isoCode] = errors;
        }
        
        //check if we want to overwrite the current translations
        if (overwrite) {          
          languages[isoCode].translations = translations;
        } else {          
          Ext.apply(languages[isoCode].translations, translations);
        }
      }
    }
  };
}();

/**
 * en-GB
 * UK English Language
 * Registers the above language into the Ext.ux.Translator.
 */
(function() {
  var englishTranslations = (function() {
    var translations = {};
    
    //uniques
    Ext.apply(translations,
      {
//Application Names
        app_address_book:                 'Address Book',
        app_audio:                        'Audio',
        app_blog:                         'Notepad',
        app_bookmarks:                    'Bookmarks',
        app_broadcast:                    'Broadcast',
        app_calendar:                     'Events',
        app_chat_room:                    'Chat Room',
        app_doodle:                       'Doodle',
        app_id_card:                      'ID Card',
        app_id_cards:                     'ID Cards',
        app_file_cabinet:                 'File Cabinet',
        app_feedback:                     'Feedback',
        app_forum:                        'Forum',
        app_boardposts:                   'Messages',
        app_overview:                     'Overview',
        app_passwords:                    'Passwords',
        app_photos:                       'Gallery',
        app_settings:                     'Settings',
        app_google_maps:                  'Google Maps',
        app_stickies:                     'Stickies',
        app_tasks:                        'Tasks',
        app_videos:                       'Videos',
        app_micro_blog:                   'Status Updates',
        app_opinion_poll:                 'Opinion Poll',
        app_rss_reader:                   'RSS Reader',
        app_browser:                      'Browser',
        app_map:                          'Map',
        app_wiki:                         'Wiki',
        app_email:                        'Email',
        app_new_id_card:                  'New ID Card',

//Stickies
        colour:                           'Color',
        stamp:                            'Stamp',
        app_sticky_destroy_desc:          'Are you sure you want to delete this sticky?',
        
//Message Board
        app_boardpost_destroy_desc:       'Are you sure you want to delete this message?',
        app_boardposts_add_description:   'add message',
        
//Blog
        app_blog_destroy_desc:            'Are you sure you want to delete this note?',

//Bookmarks
        delicious_sync:                   'Delicious Sync',
        digg_sync:                        'Digg Sync',
        app_bookmark_destroy_desc:        'Are you sure you want to delete this bookmark?',
        app_bookmarks_loading_title:      'Fetching title from URL...',

//Tasks
        app_tasks_confirm_delete:         'Delete Task',
        app_tasks_confirm_delete_desc:    new Ext.XTemplate('Are you sure you want to delete the task named: <span class="title">{title}</span>?'),
        app_task_destroy_desc:            'Are you sure you want to delete this task?',
        app_tasks_enter_description:      'description',
        app_tasks_add_assignee:           'assignee',
        app_tasks_add_due_date:           'due date',
        app_tasks_add_milestone:          'milestone',
        app_tasks_high:                   'high',
        app_tasks_medium:                 'medium',
        app_tasks_low:                    'low',
        app_tasks_completed:              'completed',
        app_tasks_pending:                'pending',
        app_tasks_started:                'started',
        app_tasks_invalid:                'rejected',
        app_tasks_overdue:                'overdue',
        app_tasks_file:                   'link to file', 
        link_to_file:                     'Link to file',
        attatched_file:                   'Attached file',
        app_tasks_null:                   'none',
        app_tasks_component:              'component',
        app_tasks_feature:                'feature',
        app_tasks_critical_bug:           'critical bug',
        app_tasks_bug:                    'bug',
        app_tasks_chore:                  'chore',
        app_tasks_admin:                  'admin',
        app_tasks_tweak:                  'tweak',
        app_tasks_design:                 'design',
        flagged:                          'Flag',
        app_tasks_flagged:                'flagged',
        app_tasks_not_flagged:            'not flagged',
        milestone:                        'Milestone',
        milestones:                       'Milestones',
        milestone_name:                   'Milestone name',
        milestone_date:                   'Milestone date',
        new_milestone:                    'new milestone',
        app_tasks_add_description:        'description',

//ID Card
        destroy_id_card:                  'Delete ID Card',
        destroy_id_card_desc:             'Are you sure you want to delete this ID Card?<br/>Any hives that use this ID Card will then use your default ID Card instead.',
        id_card_name:                     'ID Card Name',

//Photos
        upload:                           'Upload',
        uploaded_by:                      'Uploaded by',
        app_photos_album_empty:           'empty album',
        destroy_image_desc:               'Are you sure you want to delete this image?',
        destroy_album_desc:               'Are you sure you want to delete this album and all the images inside it?',
				add_from_flickr:  								'Flickr',
				flickr_url:                       'Flickr URL',
				download_original:                'view full size',
				app_photos_url_field_desc:        'type the direct URL of any image on the web',
				app_photos_flickr_field_desc:     'type the URL of any flickr image, set or user. Note: Cannot be the URL of a flickr search result.',

//Videos
        embed:                            'Embed',
        video_url:                        'Video URL',
				app_videos_field_desc:            'type the video URL from YouTube or Vimeo',
        destroy_video_desc:               'Are you sure you want to delete this video?',
        destroy_videoalbum_desc:          'Are you sure you want to delete this album and all the videos inside it?',
				
//Microblog
				twitter_sync:                     'Twitter Sync',
				app_microblog_destroy_desc:       'Are you sure you want to delete this micro blog?',
				hash_tags:                        'Hash Tags',
				remove_twitter_sync_desc:         'Are you sure you want to disable Twitter sync?',
				turn_off:                         'Turn Off',
				leave_on:                         'Leave On',
				
//File Cabinet
				destroy_file_desc:                'Are you sure you want to delete this file?', // This has a 'title' variable for the file, if needed
				destroy_files_desc:               'Are you sure you want to delete these files?',
				reached_hive_limit:               'You have reached the hive\'s storage limit of 500mb. Soon you\'ll be able to purchase extra space.',
			  file_too_large:                   'One of the files you\'re trying to upload is larger than 100mb and cannot be uploaded. All other files were uploaded successfully.',
				download_all:                     'Download',
				
				version:                          'Version',
				
				delete_comment_desc:              'Are you sure you want to delete this comment?',
				
//Comments
				comment:                          'comment',
				comments:                         'comments',
				new_comment:                      'new comment',
				
//Settings
        settings:                         'Settings',
        
        hive:                             'Hive',
        personal:                         'Personal',
        notifications:                    'Notifications',
        applications:                     'Applications',
        quick_apps:                       'Quick Apps',
        account_info:                     'Account Info',
        appearance:                       'Appearance',
        header:                           'Header',
        background:                       'Background',
				
				//info
				info:                             'Info',
				invite_url:                       'Invite URL',
				
				//account
				price_plan:                       'Price Plan',
				view_plans:                       'View Plans',
				language:                         'Language',
				
				billing_information:              'Billing Information',
				credit_card:                      'Credit Card',
				expiration_date:     							'Expiration Date',
				security_code:    								'Security Code',
				company:                          'Company',
				address: 													'Address',
				post_code: 												'Post Code',
				country:                          'Country',
        // plans_free_info:                  '0.5GB Storage, 1 Pro Hive, 3 Members / Pro Hive',
				plans_free_info:                  '0.5GB Storage, 2 Hives',
        // plans_bronze_info:                '3GB Storage, 6 Pro Hives, 10 Members / Pro Hive',
				plans_bronze_info:                '3GB Storage, 6 Hives',
        // plans_silver_info:                '12GB Storage, 12 Pro Hives, 20 Members / Pro Hive',
				plans_silver_info:                '12GB Storage, 12 Hives',
        // plans_gold_info:                  '25GB Storage, 25 Pro Hives, 60 Members / Pro Hive',
				plans_gold_info:                  '25GB Storage, 25 Hives',
				credit_card_number:               'Credit Card Number',
        credit_card_type:                 'Card Type',
        credit_card_month:                'Expiration Month',
        credit_card_year:                 'Expiration Year',
				pay:                              'Pay',
				change_password:                  'Change Password',
				old_password:                     'Old Password',
				new_password:                     'New Password',
				confirm_new_password: 						'Confirm New Password',			
				close_account:                    'Close Account',
				destroy_account:                  'Leave Kohive',
				keep_account:                     'Keep it open',
				goodbye:                          'Good Bye',
				destroy_account_desc:             'Are you sure you want to close your Kohive account? Anything you have posted into any hive will be permanently deleted and cannot be restored.',
				destroy_account_confirm_desc:     'We\'re sorry to see you go. Are you really sure you want to delete your account?',
				destroy_account_goodbye_desc:     'Your Kohive account and all content posted by you has been successfully deleted.',
				destroy_account_error_desc:       'Sorry something went wrong when deleting your account. Please try again.',
				update:                           'Update',
				next_billing_date:                'Next billing date',
				discount_code:                    'Discount code',
				
				//notifications
				email_notifications:                 'Email Notifications',
				
				notifications_invitation_to_hive:    'Invitated to hive',
				notifications_kicked_from_hive:      'Kicked from hive',
		
				notifications_task_assigned_to_me:   'Task assigned to me',
				notifications_task_of_you_completed: 'Task completed',
				notifications_message_board_mention: 'Message Board mention',
				notifications_new_private_message:   'Private Message',
				notifications_new_comment_photos:    'Comment on image',
				notifications_new_comment_blog:      'Comment on note',
				notifications_new_comment:           'Comment on an image or video',
				notifications_new_comment_task:      'Comment on a task, image or video',
				notifications_new_hive_member:       'New hive member',
				
        //id card
        id_card:                          'ID Card',
				
				//applciations
				standard_applications:            'Standard',
				pro_applications:                 'Pro',
				
//Quick Apps
        qapp_bookmarks:                   'Quick Bookmarks',
        qapp_microblog:                   'Micro Blog',
        qapp_upload:                      'Quick Upload',
        qapp_teleport:                    'Teleport',

//Widgets
        //search
        amazon:                           'Amazon',
        domain_name:                      'Domain Name',
        dictionary:                       'Dictionary',
        google:                           'Google',
        imdb:                             'IMDB',
        wikipedia:                        'Wikipedia',

//Languages
        english:                          'English',
        portuguese:                       'PortuguÃªs',
        spanish:                          'EspaÃ±ol',
        french:                           'FranÃ§ais',

//Labels
        kohive:                           'Kohive',
        on:                               'On',
        off:                              'Off',
        me:                               'Me',
        edit:                             'edit',
        none:                             'none',
        short_description:                'Short Description',
        due:                              'Due',
        due_on:                           'Due by',
        created_by:                       'Created by',
        created_on:                       'Created on',
        created_at:                       'Created at',
        completed_on:                     'Completed on',
        posted_on:                        'Posted on',
        date_created:                     'Date Created',
        current_owner:                    'Current Owner',
        invited_by:                       'Invited by',
        assignee:                         'Assignee',
        assignees:                        'Assignees',
        moderator:                        'Moderator',
        member_since:                     'Member Since',
        accept:                           'Accept',
        ddelete:                          'Delete', //dd for a reason. don't fix pls.
        remove:                           'Remove',
        keep:                             'Keep',
        decline:                          'Decline',
        message:                          'Message',
        information:                      'Information',
        nnew:                             'New', //nn for a reason. don't fix pls.
        notes:                            'Notes',
        recipient:                        'Recipient',
        recipients:                       'Recipients',
        sender:                           'Sender',
        senders:                          'Senders',
        refresh:                          'Refresh',
        tags:                             'Tags',
        title:                            'Title',
        toggle_all:                       'Toggle All',
        url:                              'URL',
        due_date:                         'Due Date',
        due_date_empty:                   'examples: tomorrow, 15 Oct, next week, in 3 days, 15/10 etc',
        priority:                         'Priority',
        assigned_to:                      'Assigned to',
        description:                      'Description',
        creator:                          'Creator',
        size:                             'Size',
        posted_by_x_on_x:                 new Ext.XTemplate('{nickname}, {date}'),
        size:                             'Size',
				picture:    											'Picture',
				save:                             'Save',
				app_nothing_to_display:           'nothing to display',
				invite:                           'Invite',
				iimport:                          'Import',
				logged_in_as_x:                   'Logged in as',
				coming_soon:                      'Coming Soon',
				kohive_logo_alt:                  '',
				oon:                              'on',
				change:                           'Change',
				everyone:                         'Everyone',
				remove:                           'Remove',
        
//+ Add
        app_add_note:                     'note',
        app_add_post:                     'post',
        app_add_description:              'description',
        app_add_tags:                     'tags',
        app_add_title:                    'title',
        app_add_file:                     'attach file',
        app_add:                          'add',
        app_new_comment:                  'New Comment',
        
//ID Card Fields     
        name:                             'Name',
        id:                               'ID',
        idcard_name:                      'ID Card Name',
        full_name:                        'Full Name',
        first_name:                       'First Name',
        last_name:                        'Last Name',
        email:                            'Email',
        password:                         'Password',
        nickname:                         'Nick Name',
        job_title:                        'Job Title',
        location:                         'Location',
        local_time:                       'Local Time',
        last_online:                      'Last Online',
        sexuality:                        'Sexuality',
        relationship:                     'Relationship',
        birthday:                         'Birthday',
        religion:                         'Religion',
        political_view:                   'Political Views',
        heritage:                         'Heritage',
        website:                          'Website',
        phone:                            'Phone',
        bio:                              'Mini Bio',
        author:                           'Author',
        authors:                          'Authors',
        gender:                           'Gender',
		    effort: 					            	  'Effort', 
		    type:   					            	  'Type', 
		    state: 					            	    'State', 
	    	avatar:                           'Avatar',
	    	favorites:                        'Favourites',
	    	activites:                        'Activities',
        topics:                           'Topics',
        foods:                            'Foods',
        diet:                             'Diet',
        drinks:                           'Drinks',
        movies:                           'Movies',
        tv_shows:                         'TV Shows',
        music:                            'Music',
        books:                            'Books',
        role_models:                      'Role Models',
        places:                           'Places',
        quotes:                           'Quotes',
        male:                             'Male',
        female:                           'Female',
        other:                            'Other',
        im:                               'IM',
        skype:                            'Skype',
        linked_in:                        'Linked In',
        twitter:                          'Twitter',
				
				verify_id_card_email:             'You should have received a verification email to confirm this email is yours.<br />Remember it may sometimes get caught in your spam folder.<br />Would you like us to send another email?',
				verify_again:                     'Verify Again',
        
//Status Bar
        app_statusbar_success:            'success!',
        app_statusbar_failure:            'something went wrong',
        app_statusbar_loading:            'loading...',
        app_statusbar_refreshing:         'refreshing...',
        app_statusbar_sorting:            'sorting...',
        app_statusbar_submitting:         'submitting...',
        app_statusbar_destroying:         'deleting...',
        app_statusbar_uploading:          'uploading...',
        app_statusbar_creating_album:     'creating album...',
        app_statusbar_updating:           'updating...',
        app_statusbar_processing:         'processing...',

				app_statusbar_double_click_to_edit: 'double click to edit',
				app_statusbar_drag_to_desktop:      'drag to desktop',
				app_statusbar_saving_to_desktop:    'saving...',

//Header Panel
        logged_in_as_x:                   new Ext.XTemplate('Logged in as {username}'),
        logout:                           'Logout',
        help:                             'Help',
        
//Desktop Stcikies
        app_desktop_stickies_destroy_desc:  'Are you sure you want to remove this sticky from the desktop? </br> It\'ll remain in the stickies application.',
				app_desktop_stickies_limit_reached: 'Sorry, you can only add a maximum of 7 stickies to the desktop.',
        
//Hive Panel
        create_first_hive:                'Choose a name for your first Hive:',
        create_first_hive_desc:           'Examples: Project, Client, Family, Holiday Plan, Vegans, Football Team, Muse Fans',
        add_new_hive:                     'Add New Hive',
        new_hive:                         'New Hive',
        hive_name:                        'Hive Name',
        hive_type:                        'Hive Type',
        hive_type_invite_only:            'Invite Only',
        hive_type_personal:               'Personal',
        hive_type_public:                 'Public',
        hive_panel_submit:                'Add',
        owner:                            'Owner',
				member_count:                     'Members',
        leave_hive:                       'Leave',
        stay_in_hive:                     'Stay',
        leave_hive_desc:                  new Ext.XTemplate('Are you sure you want to leave <span class="hive_title">{hive_title}</span>? <br />Anything you have posted will remain in the hive.'),
		    decline_hive_invite_desc: 		    'Are you sure you want to decline your invitation to join this hive?',
  	    decline_hive_invite:              'Decline',
        something_wrong:                  'Sorry, something went wrong. We will look into it.',
        add:                              'Add',
        post:                             'Post',
        date:                             'Date',
        cancel:                           'Cancel',
        kick_out:                         'Kick Out',
        keep:                             'Keep',
        decline:                          'Decline',
        uninvite:                         'Uninvite',
        leave:                            'Leave',
        stay:                             'Stay',
        leaving:                          'leaving...',
        declining:                        'declining...',
				pending_hive_bubble_desc:         'You have been invited to join this hive.', //Paul Smith has invited you to join this hive
				
//Pro hive stuff
				new_pro_hive_error:               'You need to upgrade your account to create more pro hives.',
				new_pro_hive_tooltip:             'A Pro hive includes business apps such as task management, file storage, and calendar.',
				id_card_pro_hive_error:           'You need to upgrade to a Pro account before you can create more than 2 ID Cards.',
				new_pro_hive_max_members_error:   new Ext.XTemplate('To invite more than {num} members into this Pro Hive you must upgrade your account.'),
				new_pro_hive_max_members_error_:  'Member limit reached. Please contact the owner of this Hive.',
				too_many_hives:                   'Hive limit reached',

//Invite Users
				invite_users:                      'Invite Users',
				import_authentication_error:       'Incorrect email/password',
				first_hive_new_member_desc:        'Now type the email addresses of the people you want to invite to this hive, separated by spaces. Or click above to import contacts.',
				
//Invite to hive dialog
        hive_invite_dialog_desc:           new Ext.XTemplate('If you would like to invite others into this hive you can just share this URL:<br /><div class="x-url">http://www.kohive.com/{code}</div>'),
        hive_invite_dialog_message_title:  'Here\'s a suggested message to go with it:',
        hive_invite_dialog_message_body:   new Ext.XTemplate('Visit this link to join the hive "{title}" I just created in Kohive. http://www.kohive.com/{code}'),

//Member Panel
        invite_new_member:                'Invite New Member',
        invite_member_desc:               'Invite a new person to this hive by entering their email address:',
        kick_member:                      'Kick Member',
        kick_member_desc:                 new Ext.XTemplate('Are you sure you want to kick <span class="full_name">{full_name}</span> out of this hive?<br />Anything they have posted will remain in the hive.'),
        cancel_inivitation:               'Cancel Invitation',
        cancel_inivitation_desc:          new Ext.XTemplate('Are you sure you want to uninvite <span class="full_name">{full_name}</span> from this hive?'),
        new_member_help:                  'You can invite more than one person by typing several emails into the field above.',
				message_from_x:                   new Ext.XTemplate('New message from <span class="assignee">{assignee}</span>:'),
				send_message_to_x:                new Ext.XTemplate('Send a message to <span class="assignee">{assignee}</span>:'),
				add_member_max_reached_owner:     'The maximum limit for members on this hive has been reached. You need to upgrade your account to allow more members to join.',
				add_member_max_reached_member:    'The maximum limit for members on this hive has been reached. Please contact the hive owner.',
			  resent_signup_email_success:      'This member has been sent another invitation to join this hive.',
			
//Events
				event_hive_create:                new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> created this hive'),
				event_hive_update:                '', //for when we allow hive renaming
        event_membership_accept:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> has joined this hive'),
				event_membership_create:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> has been invited to join this hive'),
				event_membership_destroy:         new Ext.XTemplate(
					'<tpl if="this.isKick(extra_new)"><b class="author_nickname">{author_nickname}</b> has been kicked from this hive<tpl if="extra_old"> by <b>{extra_old}</b></tpl></tpl>',
					'<tpl if="this.isLeft(extra_new)"><b class="author_nickname">{author_nickname}</b> left this hive</tpl>',
					'<tpl if="this.isUninvite(extra_new)"><b class="author_nickname">{author_nickname}</b> was uninvited from this hive<tpl if="extra_old"> by <b>{extra_old}</b></tpl><tpl if="extra_old"> by <b>{extra_old}</b></tpl></tpl>',
					{
						isKick: function(value) { return value == "kick"; },
						isLeft: function(value) { return value == "left"; },
						isUninvite: function(value) { return value == "uninvite"; }
					}
				),
				
				//blog
				event_blog_create:                new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> posted a new <span class="app">note</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_blog_update_title:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the title on <span class="app">note</span> post<tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_blog_update_body:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the <span class="app">note</span> post: <b class="title">{title}</b>'),
				event_blog_destroy:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted the <span class="app">note post</span>: <b class="title">{extra_old}</b>'),
				
				//bookmarks
				event_bookmark_create:            new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> added a new <span class="app">bookmark</span>: <b class="title">{title}</b>'),
				event_bookmark_update_title:      new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the title on the <span class="app">bookmark</span>: <b class="title">{title}</b>'),
				event_bookmark_update_note:       new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the note on the <span class="app">bookmark</span>: <b class="title">{title}</b>'),
				event_bookmark_update_url:        new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the url on the <span class="app">bookmark</span>: <b class="title">{title}</b>'),
				event_bookmark_destroy:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted the <span class="app">bookmark</span>: <b class="title">{extra_old}</b>'),
				
				//boardposts
				event_boardpost_create:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> posted a new <span class="app">message</span>'),
				event_boardpost_update_body:      new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the <span class="app">message</span>: <b class="title">{[values.extra_new.truncate()]}</b>'),
				event_boardpost_destroy:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted <tpl if="extra_old">the</tpl><tpl if="!extra_old">a</tpl> <span class="app">message</span><tpl if="extra_old">: <b class="title">{[values.extra_old.truncate()]}</b></tpl>'),
				
				event_comment_create:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> posted a new <span class="app">comment</span> on <tpl if="title"><b class="title">{title}</b></tpl><tpl if="!title"> an <span class="app">image</span></tpl>'),
				event_comment_update:             '',
				event_comment_destroy:            '',
				
				//images
				event_image_create:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> uploaded a new <span class="app">image</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_image_update_title:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the title on <tpl if="extra_new">the</tpl><tpl if="!extra_new">a</tpl> <span class="app">image</span><tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_image_update_description:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the description on the <span class="app">image</span>: <b class="title">{title}</b>'),
        event_image_destroy:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted an <span class="app">image</span><tpl if="extra_old">: <b class="title">{extra_old}</b></tpl>'),
				
				event_album_create:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> created a new <span class="app">image album</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_album_update:               '', //for when we allow album renaming
				event_album_destroy:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted a <span class="app">image album</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				
				//microblog
				event_microblog_create:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> posted a new <span class="app">micro blog</span><tpl if="extra_new">: <b class="title">{[values.extra_new.truncate()]}</b></tpl>'),
				event_microblog_update_body:      new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the <span class="app">micro blog</span>: <b class="title">{[values.extra_new.truncate()]}</b>'),
				event_microblog_destroy:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted the <span class="app">micro blog</span>: <b class="title">{[values.extra_old.truncate()]}</b>'),
				
				//stickies
				event_sticky_create:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> posted a new <span class="app">sticky</span><tpl if="extra_new">: <b class="title">{[values.extra_new.truncate()]}</b></tpl>'),
				event_sticky_update_body:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the <span class="app">sticky</span>: <b class="title">{[values.extra_new.truncate()]}</b>'),
				event_sticky_destroy:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted the <span class="app">sticky</span>: <b class="title">{[values.extra_old.truncate()]}</b>'),
				
				//videos
				event_video_create:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> uploaded a new <span class="app">video</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_video_update_title:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the title on <tpl if="extra_new">the</tpl><tpl if="!extra_new">a</tpl> <span class="app">video</span><tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_video_update_description:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the description on the <span class="app">video</span>: <b class="title">{title}</b>'),
				event_video_destroy:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted a <span class="app">video</span><tpl if="title">: <b class="title">{extra_old}</b><tpl>'),
				
				event_videoalbum_create:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> created a new <span class="app">video album</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_videoalbum_update:          '', //for when we allow album renaming
				event_videoalbum_destroy:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted a <span class="app">image album</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				
				//files
				event_filecabinet_create:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> uploaded a new <span class="app">file</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_filecabinet_update_title:       new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the title on <tpl if="extra_new">the</tpl><tpl if="!extra_new">a</tpl> <span class="app">file</span><tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_filecabinet_update_description: new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the description on the <span class="app">file</span>: <b class="title">{title}</b>'),
				event_filecabinet_destroy:            new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted the <span class="app">file</span>: <b class="title">{extra_old}</b>'),
				
				//tasks
				event_task_create:                  new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> created a new <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_title:            new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the title on the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_type:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> changed the type of the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_body:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the description on the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_priority:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> changed priority to {extra_new} on the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_effort:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> changed effort to {extra_new} on the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_status_pending:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> flagged/unflagged the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_flagged:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> edited the description on the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_status_completed: new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> completed the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_status_invalid:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> invalidated the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_status_new:       new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> uncompleted the <span class="app">task</span>: <b class="title">{title}</b>'),
				event_task_update_assignee:         new Ext.XTemplate(
					'<tpl if="extra_old && extra_new"><b class="author_nickname">{author_nickname}</b> reassigned from {extra_old} to {extra_new} on the <span class="app">task</span>: <b class="title">{title}</b></tpl>',
					'<tpl if="extra_old && !extra_new"><b class="author_nickname">{author_nickname}</b> unassigned {extra_old} from the <span class="app">task</span>: <b class="title">{title}</b></tpl>',
					'<tpl if="!extra_old && extra_new"><b class="author_nickname">{author_nickname}</b> assigned {extra_new} to the <span class="app">task</span>: <b class="title">{title}</b></tpl>'
				),
				event_task_update_due:              new Ext.XTemplate(
				  '<b class="author_nickname">{author_nickname}</b> changed the due date to {[this.convertDate(values.extra_new)]} on the <span class="app">task</span>: <b class="title">{title}</b>',
				  {
				    convertDate: function(date) {
              return Kohive.OS.convertToDateSpecial(date, true);
				    }
				  }
				),
				event_task_update_overdue:          '', //This task is now overdue: {title}
				event_task_destroy:                 new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deleted the <span class="app">task</span>: <b class="title">{extra_old}</b>')
      }
    );
    
    Ext.apply(translations,
      //duplicates, do not touch these Edward
      {
        app_microblogs:      translations.app_micro_blog,
        app_board_posts:     translations.app_boardposts,
				
				app_filecabinet:     translations.app_file_cabinet,
				
        app_chatroom:        translations.app_chat_room,
				
				app_privatemessages: translations.app_private_messages,
				
				app_addressbook:     translations.app_address_book,
				
				app_opinionpoll:     translations.app_opinion_poll,
				
				app_googlemaps:      translations.app_google_maps
      }
    );
    
    return translations;
  })();
  
  Ext.ux.Translator.registerLanguage('en-GB', 'UK English', englishTranslations);
})();

/**
 * fr
 * French
 * Registers the above language into the Ext.ux.Translator.
 */
Ext.ux.Translator.registerLanguage('fr', 'FranÃ§ais', {
  
  /**
   * Global
   */
  kohive:                           'Kohive'
  
});

/**
 * pt-BR
 * Brazilian Portuguese
 * Registers the above language into the Ext.ux.Translator.
 */
(function() {
  var portugueseTranslations = (function() {
    var translations = {};
    
    //uniques
    Ext.apply(translations,
      {
//Application Names
        app_address_book:                 'Agenda',
        app_audio:                        'Audio',
        app_blog:                         'Bloco',
        app_bookmarks:                    'Favoritos',
        app_broadcast:                    'VÃ­deo ao vivo',
        app_calendar:                     'Eventos',
        app_chat_room:                    'Chat',
        app_doodle:                       'Rabiscos',
        app_id_card:                      'Identidade',
        app_id_cards:                     'Identidades',
        app_file_cabinet:                 'Arquivos',
        app_feedback:                     'Feedback',
        app_forum:                        'Forum',
        app_boardposts:                   'Mensagens',
        app_overview:                     'Resumo',
        app_passwords:                    'Senhas',
        app_photos:                       'GalerÃ­a',
        app_settings:                     'ConfiguraÃ§Ãµes',
        app_google_maps:                  'Google Maps',
        app_stickies:                     'Notinhas',
        app_tasks:                        'Tarefas',
        app_videos:                       'Videos',
        app_micro_blog:                   'Micro Blog',
        app_opinion_poll:                 'OpiniÃµes',
        app_rss_reader:                   'Leitor RSS',
        app_browser:                      'Navegador',
        app_map:                          'Mapa',
        app_wiki:                         'Wiki',
        app_email:                        'Email',
        app_new_id_card:                  'Nova identidade',
//Application Names (I think these are wrong as they aren't separated by _ )        
        app_filecabinet:                  'Arquivos',
      	app_chatroom:                     'Chat',
      	app_privatemessages:              'Mensagens privadas',
      	app_addressbook:                  'Agenda de endereÃ§os',
      	app_opinionpoll:                  'OpiniÃ£o',
      	app_googlemaps:                   'Google maps',

//Stickies
        colour:                           'Cor',
        stamp:                            'Carimbo',
        app_sticky_destroy_desc:          'VocÃª tem certeza que quer apagar esta notinha?',
        
//Videos
        video_url:                        'VÃ­deo URL',
				app_videos_field_desc:            'digite o endereÃ§o de um vÃ­deo do YouTube ou Vimeo',
        destroy_video_desc:               'VocÃª tem certeza que quer apagar este vÃ­deo?',
        destroy_videoalbum_desc:          'VocÃª tem certeza que quer apagar este album e todos os vÃ­deos dentro dele?',
        
//Message Board
        app_boardpost_destroy_desc:       'VocÃª tem certeza que quer apagar esta mensagem?',
        
//Blog
        app_blog_destroy_desc:            'VocÃª tem certeza que quer apagar esta nota?',

//Bookmarks
        delicious_sync:                   'Delicious Sync',
        digg_sync:                        'Digg Sync',
        app_bookmark_destroy_desc:        'VocÃª tem certeza que quer apagar este favorito?',
        app_bookmarks_loading_title:      'Procurando tÃ­tulo da pÃ¡gina...',

//Tasks
        app_tasks_confirm_delete:         'Deletar terefa',
        app_tasks_confirm_delete_desc:    new Ext.XTemplate('Are you sure you want to delete the task named: <span class="title">{title}</span>?'),
        app_task_destroy_desc:            'Are you sure you want to delete this task?',
        app_tasks_enter_description:      'descriÃ§Ã£o',
        app_tasks_add_assignee:           'recipiente',
        app_tasks_add_due_date:           'data',
        app_tasks_high:                   'alto',
        app_tasks_medium:                 'mÃ©dio',
        app_tasks_low:                    'baixo',
        app_tasks_completed:              'completa',
        app_tasks_pending:                'nova',
        app_tasks_started:                'comeÃ§ada',
        app_tasks_invalid:                'rejeitada',
        app_tasks_overdue:                'atrasada',
        app_tasks_file:                   'anexar arquivo', 
        link_to_file:                     'Anexar arquivo',
        attatched_file:                   'Arquivo anexado',

//ID Card
        destroy_id_card:                  'Deletar identidade',
        destroy_id_card_desc:             'VocÃª tem certeza que quer apagar esta identidade?<br/>Todas as colmÃ©ias que usarem esta identidade passarÃ£o a usar a identidade padrÃ£o.',
        id_card_name:                     'Nome da identidade',

//Photos
        upload:                           'Enviar',
        uploaded_by:                      'Enviado por',
        app_photos_album_empty:           'album vazio',
        destroy_image_desc:               'VocÃª tem certeza que quer apagar esta imagem?',
        destroy_album_desc:               'VocÃª tem certeza que quer apagar este album e todas as imagens dentro dele?',
				add_from_flickr:  								'Flickr',
				flickr_sync:  										'Flickr Sync',
			  flickr_url:                       'Flickr URL',
			  app_photos_url_field_desc:        'digite o endereÃ§o de qualquer imagem na internet',
				app_photos_flickr_field_desc:     'digite o endereÃ§o de qualquer imagem, set ou usuÃ¡rio do flickr.',
				
//Microblog
				twitter_sync:                     'Twitter Sync',
				app_microblog_destroy_desc:       'VocÃª tem certeza que quer apagar este micro blog?',
				
//File Cabinet
				destroy_file_desc:                'VocÃª tem certeza que quer apagar este arquivo?', // This has a 'title' variable for the file, if needed
				destroy_files_desc:               'VocÃª tem certeza que quer apagar estes arquivos?',
				reached_hive_limit:               'VocÃª chegou ao limite de capacidade desta colmeia que guarda atÃ© 500mb. Em breve vocÃª podera comprar mais espaÃ§o.',
			  file_too_large:                   'Um dos arquivos que voce estÃ¡ tentando enviar e maior que o limite permitido de 100mb por arquivo e nao pode ser enviado. Todos os outros arquivos foram enviados corretamente.',
			  download_all:                     'Baixar',
				
//Settings
        hive:                             'ColmÃ©ia',
        personal:                         'Pessoal',
        notifications:                    'NotificaÃ§Ãµes',
        applications:                     'Aplicativos',
        quick_apps:                       'Quick Apps',
        account_info:                     'InformaÃ§Ã£o',
        appearance:                       'AparÃªncia',
        header:                           'CabeÃ§Ã¡rio',
        background:                       'Fundo',
				//account
				change_password:                  'Modificar senha',
				old_password:                     'Senha antiga',
				new_password:                     'Nova senha',
				confirm_new_password: 						'Confirme a nova senha',
				
				close_account:                    'Fechar Conta',
				destroy_account:                  'Sair da Kohive',
				keep_account:                     'Ficar aberto',
				goodbye:                          'Tchau!',
				destroy_account_desc:             'VocÃª tem certeza que quer fechar a sua conta da Kohive? Qualquer coisa que vocÃª colocou em qualquer colmÃ©ia poderÃ¡ ser permanentemente apagado e nÃ£o poderÃ¡ ser restaurado.',
				destroy_account_confirm_desc:     'VocÃª tem certeza que quer deletar sua conta?',
				destroy_account_goodbye_desc:     'Sua conta Kohive e todo conteÃºdo enviado por vocÃª foi apagado.',
				destroy_account_error_desc:       'Nos perdoe, algo de errado houve quando tentamos apagar sua conta. Por favor tente novamente.',
				update:                           'Atualize',
				
				//notifications
				email_notifications:                 'Emails',
				
				notifications_invitation_to_hive:    'Convidado para uma colmÃ©ia',
				notifications_kicked_from_hive:      'Expulsado de uma colmÃ©ia',
		
				notifications_task_assigned_to_me:   'Tarefa para mim',
				notifications_message_board_mention: 'MensÃ¡gem para mim',
				notifications_new_private_message:   'MensÃ¡gem privada',
				notifications_new_comment_photos:    'ComentÃ¡rio numa imagem',
				notifications_new_comment_blog:      'ComentÃ¡rio num blog',
				notifications_new_hive_member:       'Novo mebro na colmÃ©ia',
				
        //id card
        id_card:                          'Identidade',
        //applciations
				standard_applications:            'PadrÃ£o',
				pro_applications:                 'Pro',

//Quick Apps
        qapp_bookmarks:                   'Quick Bookmarks',
        qapp_microblog:                   'Micro Blog',
        qapp_upload:                      'Quick Upload',
        qapp_teleport:                    'Teleport',

//Widgets
        //search
        amazon:                           'Amazon',
        domain_name:                      'DomÃ­nio',
        dictionary:                       'DicionÃ¡rio',
        google:                           'Google',
        imdb:                             'IMDB',
        wikipedia:                        'Wikipedia',

//Languages
        english:                          'English',
        portuguese:                       'PortuguÃªs',
        spanish:                          'EspaÃ±ol',
        french:                           'FranÃ§ais',

//Labels
        kohive:                           'Kohive',
        on:                               'Ligado',
        off:                              'Desligado',
        me:                               'Eu',
        edit:                             'editar',
        none:                             'ninguÃ©m',
        short_description:                'DescriÃ§Ã£o curta',
        due:                              'Devido',
        due_on:                           'Devido em',
        created_by:                       'Criado por',
        created_on:                       'Criado em',
        created_at:                       'Criado as',
        posted_on:                        'Enviado em',
        date_created:                     'Data criado',
        current_owner:                    'Dono atual',
        invited_by:                       'Convidado por',
        assignee:                         'Recipiente',
        assignees:                        'Recipientes',
        moderator:                        'Moderador',
        member_since:                     'Membro desde',
        accept:                           'Aceitar',
        ddelete:                          'Deletar', //dd for a reason. don't fix pls.
        keep:                             'Manter',
        decline:                          'Rejeitar',
        message:                          'Mensagem',
        information:                      'InformaÃ§Ã£o',
        nnew:                             'Novo', //nn for a reason. don't fix pls.
        notes:                            'Notas',
        recipient:                        'Recipiente',
        recipients:                       'Recipientes',
        sender:                           'Remetente',
        senders:                          'Remetentes',
        refresh:                          'Recarregar',
        tags:                             'Etiquetas',
        title:                            'TÃ­tulo',
        toggle_all:                       'Minimizar',
        url:                              'EndereÃ§o',
        due_date:                         'Data Devida',
        priority:                         'Prioridade',
        assigned_to:                      'Para',
        description:                      'DescriÃ§Ã£o',
        creator:                          'Criador',
        size:                             'Tamanho',
        posted_by_x_on_x:                 new Ext.XTemplate('{nickname}, {date}'),
				picture:    											'Foto',
				save:                             'Salvar',
				app_nothing_to_display:           'nada para mostrar',
				invite:                           'Convidar',
				iimport:                          'Importar',
				coming_soon:                      'em breve',
				kohive_logo_alt:                  '',
				oon:                              'em',
				change:                           'Modifique',
				remove:                           'Remova',
        
//+ Add
        app_add_note:                     'nota',
        app_add_post:                     'artigo',
        app_add_description:              'descriÃ§Ã£o',
        app_add_tags:                     'etiqueta',
        app_add_title:                    'tÃ­tulo',
        app_add:                          'adicionar',
        app_new_comment:                  'Novo ComentÃ¡rio',
        app_add_file:                     'anexar arquivo',
        
//ID Card Fields     
        name:                             'Nome',
        id:                               'Identidade',
        idcard_name:                      'Nome da identidade',
        full_name:                        'Nome Completo',
        first_name:                       'Primeiro Nome',
        last_name:                        'Sobrenome',
        email:                            'Email',
        password:                         'Senha',
        nickname:                         'Apelido',
        job_title:                        'ProfissÃ£o',
        location:                         'Localidade',
        local_time:                       'Hora Local',
        last_online:                      'Online em',
        sexuality:                        'Sexualidade',
        relationship:                     'Relacionamento',
        birthday:                         'AniversÃ¡rio',
        religion:                         'ReligiÃ£o',
        political_view:                   'PolÃ­tica',
        heritage:                         'Nacionalidade',
        website:                          'Website',
        phone:                            'Telefone',
        bio:                              'BiografÃ­a',
        author:                           'Autor',
        state:                            'Estato',
        authors:                          'Autores',
        gender:                           'GÃªnero',
		    effort: 					            	  'EsforÃ§o', 
	    	avatar:                           'Avatar',
	    	favorites:                        'Favoritos',
	    	activites:                        'Atividades',
        topics:                           'TÃ³picos',
        foods:                            'Comida',
        diet:                             'Dieta',
        drinks:                           'Bebida',
        movies:                           'Filmes',
        tv_shows:                         'Programa de TV',
        music:                            'MÃºsica',
        books:                            'Livros',
        role_models:                      'Pessoas',
        places:                           'Lugares',
        quotes:                           'Ditados',
        male:                             'Masculino',
        female:                           'Feminino',
        other:                            'Outro',
        im:                               'Chat',
        skype:                            'Skype',
        linked_in:                        'Linked In',
        twitter:                          'Twitter',
        
//Status Bar
        app_statusbar_success:            'sucesso!',
        app_statusbar_failure:            'algo errado',
        app_statusbar_loading:            'carregando...',
        app_statusbar_refreshing:         'recaregando...',
        app_statusbar_sorting:            'rearrumando...',
        app_statusbar_submitting:         'enviando...',
        app_statusbar_destroying:         'apagando...',
        app_statusbar_uploading:          'enviando...',
        app_statusbar_creating_album:     'criando album...',
        app_statusbar_updating:           'atualizando...',
        app_statusbar_processing:         'processando...',
        app_statusbar_drag_to_desktop:    'arraste para mesa',
       	app_statusbar_double_click_to_edit:     'clique duas vezes para editar',

//Header Panel
        logged_in_as_x:                   new Ext.XTemplate('Conta {username}'),
        logout:                           'Sair',
        help:                             'Ajuda',
          
//Hive Panel
        add_new_hive:                     'Adicionar ColmÃ©ia',
        hive_name:                        'Nome',
        hive_type:                        'Tipo',
        hive_type_invite_only:            'Privado',
        hive_type_personal:               'Pessoal',
        hive_type_public:                 'PÃºblica',
        hive_panel_submit:                'Adicionar',
        owner:                            'Dono',
				member_count:                     'Membros',
        leave_hive:                       'Sair',
        stay_in_hive:                     'Ficar',
        leave_hive_desc:                  new Ext.XTemplate('VocÃª tem certeza que quer sair de <span class="hive_title">{hive_title}</span>? <br />Tudo que publicou permanecerÃ¡ nela.'),
		    decline_hive_invite_desc: 		    'VocÃª tem certeza que quer rejeitar o convite de entrar nesta colmÃ©ia?',
  	    decline_hive_invite:              'Rejeitar',
        something_wrong:                  'Nos perdoe, houve algo de errado. NÃ³s iremos investigar.',
        add:                              'Adicionar',
        post:                             'Publicar',
        date:                             'Data',
        cancel:                           'Cancelar',
        kick_out:                         'Expulsar',
        keep:                             'Manter',
        decline:                          'Rejeitar',
        uninvite:                         'Desconvidar',
        leave:                            'Sair',
        stay:                             'Ficar',
        leaving:                          'saindo...',
        declining:                        'rejeitando...',
        pending_hive_bubble_desc:         'VocÃª foi convidado para esta colmÃ©ia.',
        
//Uncategorised
  discount_code:                          'CÃ³digo de desconto',
  app_statusbar_saving_to_desktop:        'salvando...',
	verify_id_card_email:                   'Verificar email da identidade',
	verify_again:                           'Verificar denovo',
	create_first_hive:                      'Escolha um nome para sua primeira colmÃ©ia',
	create_first_hive_desc:                 'Exemplos: Trabalho, Clientes, Projeto, FamÃ­lia, Viagem, Veganos, Time de Futebol',
	app_tasks_add_milestone:                'marco',
  app_tasks_null:                         'nada',
  app_tasks_component:                    'componente', 
  app_tasks_feature:                      'funÃ§Ã£o',
  app_tasks_critical_bug:                 'bug crÃ­tico',
  app_tasks_bug:                          'bug',
  app_tasks_chore:                        'tarefa',
  app_tasks_admin:                        'administraÃ§Ã£o',
  app_tasks_tweak:                        'ajuste',
  app_tasks_design:                       'design',
  flagged:                                'sinalizado',
  app_tasks_flagged:                      'sinalizado',
  app_tasks_not_flagged:                  'nÃ£o sinalizado',
  milestone:                              'marco',
  milestones:                             'marcos',
  milestone_name:                         'Nome do marco',
  milestone_date:                         'Data do marco',
  new_milestone:                          'novo marco',
  download_original:                      'baixar original',
  hash_tags:                              'hashtags',
  remove_twitter_sync_desc:               'VocÃª tem certeza que quer desativar sincronia com Twitter?',
  turn_off:                               'Desativar',
  leave_on:                               'Deixar ativado',
  delete_comment_desc:                    'VocÃª tem certeza que quer apagar este comentÃ¡rio?',
  comment:                                'comentÃ¡rio',
  comments:                               'comentÃ¡rios',
  new_comment:                            'novo comentÃ¡rio',
  info:                                   'Info',
  invite_url:                             'EndereÃ§o de convite',
  price_plan:                             'Plano',
  view_plans:                             'Ver Plano',
  billing_information:                    'InformaÃ§Ã£o de conta',
  credit_card:                            'CartÃ£o de crÃ©dito',
  expiration_date:                        'Data de expiraÃ§Ã£o',
  security_code:                          'CÃ³digo de seguranÃ§a',
  company:                                'Compania',
  address:                                'EndereÃ§o',
  post_code:                              'CEP',
  country:                                'PaÃ­s',
 	plans_free_info:                        '0.5GB armazenamento, 1 colmÃ©ia pro, 3 membros / colmÃ©ia pro',
	plans_bronze_info:                      '3GB armazenamento, 6 colmÃ©ias pro, 10 membros / colmÃ©ia pro',
	plans_silver_info:                      '12GB armazenamento, 12 colmÃ©ias pro, 20 membros / colmÃ©ia pro',
	plans_gold_info:                        '25GB armazenamento, 25 colmÃ©ias pro, 60 membros / colmÃ©ia pro',
  credit_card_number:                     'NÃºmero do cartÃ£o',
  credit_card_type:                       'Tipo do cartÃ£o',
  credit_card_month:                      'MÃªs de expiraÃ§Ã£o',
  credit_card_year:                       'Ano de expiraÃ§Ã£o',
  pay:                                    'Pagar',
  next_billing_date:                      'PrÃ³xima cobranÃ§a',
  notifications_task_of_you_completed:    'Tarefa completa',
  notifications_new_comment:              'ComentÃ¡rio numa imagem ou vÃ­deo',
  completed_on:                           'Completo em',
  language:                               'LÃ­ngua',
  due_date_empty:                         '',
  kohive_logo_alt:                        '',
  type:                                   'Tipo',
  hive_invite_dialog_desc:                new Ext.XTemplate('Se quizer convidar outros para esta colmÃ©ia vocÃª pode compartilhar este endereÃ§o:<br /><div class="x-url">http://www.kohive.com/{code}</div>'),
  hive_invite_dialog_message_title:      'Mensagem sugerida:',
  hive_invite_dialog_message_body:        new Ext.XTemplate('Visite este endereÃ§o para entrar na colmÃ©ia "{title}" que eu criei na Kohive. http://www.kohive.com/{code}'),
  add_member_max_reached_owner:           'O limite de membros desta colmÃ©ia foi atingido. Ã‰ necessÃ¡rio atualizar sua conta para permitir mais membros.',
  add_member_max_reached_member:          'O limite de membros desta colmÃ©ia foi atingido. Por favor contate o dono desta colmÃ©ia.',
  event_hive_update:                      '',
  event_comment_update:                   '',
  event_comment_destroy:                  '',
  event_album_update:                     '',
  event_videoalbum_create:                '',
  event_videoalbum_update:                '',
  event_videoalbum_destroy:               '',
	event_task_update_due:              new Ext.XTemplate(
	  '<b class="author_nickname">{author_nickname}</b> mudou a data para {[this.convertDate(values.extra_new)]} na <span class="app">tarefa</span>: <b class="title">{title}</b>',
	  {
	    convertDate: function(date) {
        return Kohive.OS.convertToDateSpecial(date, true);
	    }
	  }
	),
  event_task_update_overdue:              '',
	
//Pro hive stuff
	new_pro_hive_error:                     'Ã‰ necessario aumentar seu plano para criar mais colmÃ©ias pro.<br />(vindo em breve)',
	new_pro_hive_tooltip:                   'ColmÃ©ias pro incluem aplicativos para empresas, tais como tarefas, arquivos e calendÃ¡rio.',
	id_card_pro_hive_error:                 'Ã‰ necessario aumentar seu plano para criar mais de 2 identidades. (vindo em breve)',
	new_pro_hive_max_members_error:         new Ext.XTemplate('Para convidar mais de {num} membros para esta colmÃ©ia Ã© necessario aumentar seu plano. (vindo em breve)'),
	new_pro_hive_max_members_error_:        'O limite de mebros foi atingido. Por favor contate o dono desta colmÃ©ia.',
	
//Invite Users
	invite_users:                           'Convide membros',
	import_authentication_error:            'Email ou senha incorretos',
	first_hive_new_member_desc:             'Agora digite os endereÃ§os de email das pessoas que deseja convidar para esta colmÃ©ia, separados por espaÃ§os. Ou clique acima para importar contatos.',

//Member Panel
        invite_new_member:                'Convidar novo membro',
        invite_member_desc:               'Digite o endereÃ§o de email da pessoa que deseja convidar para esta colmÃ©ia:',
        kick_member:                      'Expulsar  membro',
        kick_member_desc:                 new Ext.XTemplate('VocÃª tem certeza que quer expulsar <span class="full_name">{full_name}</span> desta colmÃ©ia?<br />Tudo que publicaro permanecerÃ¡ nela'),
        cancel_inivitation:               'Cancelar convite',
        cancel_inivitation_desc:          new Ext.XTemplate('VocÃª tem certeza que quer desconvidar <span class="full_name">{full_name}</span> desta colmÃ©ia?'),
        new_member_help:                  'VocÃª pode convidar mais de uma pessoa digitando vÃ¡rios emails na caixa acima.',
        message_from_x:                   new Ext.XTemplate('Nova mensagem de <span class="assignee">{assignee}</span>:'),
				send_message_to_x:                new Ext.XTemplate('Enviar mensagem para <span class="assignee">{assignee}</span>:'),
    		resent_signup_email_success:      'Este membro foi enviado um novo convite para entrar nesta colmÃ©ia.',
    
//Events
				event_hive_create:                new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> criou esta colmÃ©ia'),
				event_hive_update:                '', //for when we allow hive renaming
				event_membership_create:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> entrou nesta colmÃ©ia'),
				event_membership_invite:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> foi convidado para entrar nesta colmÃ©ia por <b>{extra_old}</b>'),
				event_membership_destroy:         new Ext.XTemplate(
					'<tpl if="this.isKick(extra_new)"><b class="author_nickname">{author_nickname}</b> foi expulso desta colmÃ©ia<tpl if="extra_old"> por <b>{extra_old}</b></tpl></tpl>',
					'<tpl if="this.isLeft(extra_new)"><b class="author_nickname">{author_nickname}</b> saiu desta colmÃ©ia</tpl>',
					'<tpl if="this.isUninvite(extra_new)"><b class="author_nickname">{author_nickname}</b> foi desconvidado desta colmÃ©ia<tpl if="extra_old"> por <b>{extra_old}</b></tpl><tpl if="extra_old"> por <b>{extra_old}</b></tpl></tpl>',
					{
						isKick: function(value) { return value == "kick"; },
						isLeft: function(value) { return value == "left"; },
						isUninvite: function(value) { return value == "uninvite"; }
					}
				),

				//blog
				event_blog_create:                new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> escreveu um novo <span class="app">nota</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_blog_update_title:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o tÃ­tulo no <span class="app">nota</span><tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_blog_update_body:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o <span class="app">nota</span>: <b class="title">{title}</b>'),
				event_blog_destroy:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou o <span class="app">nota</span>: <b class="title">{extra_old}</b>'),

				//bookmarks
				event_bookmark_create:            new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> adicionou um novo <span class="app">favorito</span>: <b class="title">{title}</b>'),
				event_bookmark_update_title:      new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o tÃ­tulo no <span class="app">favorito</span>: <b class="title">{title}</b>'),
				event_bookmark_update_note:       new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou a descriÃ§Ã£o no <span class="app">favorito</span>: <b class="title">{title}</b>'),
				event_bookmark_update_url:        new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o endereÃ§o no <span class="app">favorito</span>: <b class="title">{title}</b>'),
				event_bookmark_destroy:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou o <span class="app">favorito</span>: <b class="title">{extra_old}</b>'),

				//boardposts
				event_boardpost_create:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> escreveu uma nova <span class="app">mensagem</span>'),
				event_boardpost_update_body:      new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou a <span class="app">mensagem</span>: <b class="title">{[values.extra_new.truncate()]}</b>'),
				event_boardpost_destroy:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou a <span class="app">mensagem</span>: <b class="title">{[valus.extra_old.truncate()}</b>'),

				event_comment_create:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> escreveu um novo <span class="app">comentÃ¡rio</span> em <tpl if="title"><b class="title">{title}</b></tpl><tpl if="!title"> uma <span class="app">image</span></tpl>'),
				event_comment_update:             '',
				event_comment_destroy:            '',

				//images
				event_image_create:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> enviou uma nova <span class="app">imagem</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_image_update_title:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o tÃ­tulo <tpl if="extra_new">da</tpl><tpl if="!extra_new">de uma</tpl> <span class="app">imagem</span><tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_image_update_description:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> ediou a descriÃ§Ã£o da <span class="app">imagem</span>: <b class="title">{title}</b>'),
				event_image_destroy:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou a <span class="app">imagem</span><tpl if="title">: <b class="title">{extra_old}</b><tpl>'),

				event_album_create:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> criou um novo <span class="app">Ã¡lbum de imagens</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_album_update:               '', //for when we allow album renaming
				event_album_destroy:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou o <span class="app">Ã¡lbum de imagens</span><tpl if="title">: <b class="title">{title}</b></tpl>'),

				//microblog
				event_microblog_create:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> escreveu um novo <span class="app">micro blog</span><tpl if="extra_new">: <b class="title">{[values.extra_new.truncate()]}</b></tpl>'),
				event_microblog_update_body:      new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o <span class="app">micro blog</span>: <b class="title">{[values.extra_new.truncate()]}</b>'),
				event_microblog_destroy:          new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou o <span class="app">micro blog</span>: <b class="title">{[valus.extra_old.truncate()}</b>'),

				//stickies
				event_sticky_create:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> escreveu uma nova <span class="app">notinha</span><tpl if="extra_new">: <b class="title">{[values.extra_new.truncate()]}</b></tpl>'),
				event_sticky_update_body:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou a <span class="app">notinha</span>: <b class="title">{[values.extra_new.truncate()]}</b>'),
				event_sticky_destroy:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou a <span class="app">notinha</span>: <b class="title">{[valus.extra_old.truncate()}</b>'),
				app_desktop_stickies_destroy_desc: 'VocÃª tem certeza que quer remover esta notinha da mesa?</br>Ela continuarÃ¡ no aplicativo de notinhas.',

				//videos
				event_video_create:               new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> enviou um novo <span class="app">video</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_video_update_title:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o tÃ­tulo <tpl if="extra_new">no</tpl><tpl if="!extra_new">em um</tpl> <span class="app">video</span><tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_video_update_description:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou a descriÃ§Ã£o no<span class="app">video</span>: <b class="title">{title}</b>'),
				event_video_destroy:              new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou o <span class="app">video</span><tpl if="title">: <b class="title">{extra_old}</b><tpl>'),

				//files
				event_filecabinet_create:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> enviou um novo <span class="app">arquivo</span><tpl if="title">: <b class="title">{title}</b></tpl>'),
				event_filecabinet_update_title:       new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o tÃ­tulo <tpl if="extra_new">no</tpl><tpl if="!extra_new">em um</tpl> <span class="app">arquivo</span><tpl if="extra_new">: <b class="title">{extra_new}</b></tpl>'),
				event_filecabinet_update_description: new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou a descriÃ§Ã£o no <span class="app">arquivo</span>: <b class="title">{title}</b>'),
				event_filecabinet_destroy:            new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou o <class="app">arquivo</span>: <b class="title">{extra_old}</b>'),

				//tasks
				event_task_create:                  new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> criou uma nova <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_title:            new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou o tÃ­tulo na <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_body:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> editou a descriÃ§Ã£o na <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_type:             new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> mudou o tipo da <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_priority:         new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> mudou a prioridade para {extra_new} na <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_effort:           new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> mudou o esforÃ§o para {extra_new} na <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_status_pending:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> comeÃ§ou a <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_status_completed: new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> completou a <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_status_invalid:   new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> invalidou a <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_status_new:       new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> descompletou a <span class="app">tarefa</span>: <b class="title">{title}</b>'),
				event_task_update_assignee:         new Ext.XTemplate(
					'<tpl if="extra_old && extra_new"><b class="author_nickname">{author_nickname}</b> mudou o recipiente de {extra_old} para {extra_new} na <span class="app">tarefa</span>: <b class="title">{title}</b></tpl>',
					'<tpl if="extra_old && !extra_new"><b class="author_nickname">{author_nickname}</b> removeu o recipiente {extra_old} da <span class="app">tarefa</span>: <b class="title">{title}</b></tpl>',
					'<tpl if="!extra_old && extra_new"><b class="author_nickname">{author_nickname}</b> registrou o recipiente {extra_new} a <span class="app">tarefa</span>: <b class="title">{title}</b></tpl>'
				),
				event_task_update_overdue:          '', //This task is now overdue: {title}
				event_task_destroy:                 new Ext.XTemplate('<b class="author_nickname">{author_nickname}</b> deletou a<span class="app">tarefa</span>: <b class="title">{extra_old}</b>')
      }
    );
    
    Ext.apply(translations,
      //duplicates, do not touch these Edward
      {
        app_microblogs:  translations.app_micro_blog,
        app_board_posts: translations.app_boardposts
      }
    );
    
    return translations;
  })();
  
  Ext.ux.Translator.registerLanguage('pt-BR', 'PortuguÃªs Brasileiro', portugueseTranslations);
})();

/**
 * @class Ext.ux.UrlBuilder
 * Builds URLs...
 */
Ext.ux.UrlBuilder = function() {
  
};

Ext.ux.UrlBuilder.prototype = {
  /**
   * @property hosts
   * @type Object
   * An array of different host types we can have
   */
  hosts: {
    'resources': 'http://resources.dev.kohive.local/'
  },
  
  /**
   * Function description
   * @param {String} host The type of URL we need to build
   * @param {String} append The string we want to append to the end of the URL
   * @return {String} The appened URL
   */
  build: function(host, append) {
    // Check if the rsources have been generated if not use the default hosts
    if (typeof KohiveResourceUrls != "undefined") {
      this.hosts = KohiveResourceUrls;
    };
    
    // Check if the host is defined
    // @see hosts
    if (!this.hosts[host]) {
      return;
    };
    
    // TODO always http?  what about https?
    var scheme = 'http';
    // Return the appended URL
    // return scheme + '://' + this.hosts[host] + '/' + append;
    return append;
  }
};

Ext.ux.UrlBuilder = new Ext.ux.UrlBuilder();

Ext.ns('Ext.ux.tree');

/**
 * Ext.ux.tree.SettingsTreeNode
 * @extends Ext.tree.TreeNode
 * Extended version of TreeNode which fires meaningful events when interacted with
 */
Ext.ux.tree.SettingsTreeNode = function(config) {
  var config = config || {};
  
  if (!config.id) {
    throw new Error("Each settings tree node must have an ID which matches the ID of a panel in the Settings app's SlidingCardLayout");
  }
 
  Ext.applyIf(config, {
    allowChildren: false
  });
  
  Ext.ux.tree.SettingsTreeNode.superclass.constructor.call(this, config);  
};
Ext.extend(Ext.ux.tree.SettingsTreeNode, Ext.tree.TreeNode);

Ext.reg('settings_treenode', Ext.ux.tree.SettingsTreeNode);

/**
 * @class Kohive.models.Application
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Application", {
  fields:    [
    {name: 'id',              type: 'int'},
    {name: 'title',           type: 'string'},
    {name: 'id_default',      type: 'string'},
    {name: 'last_updated_at', type: 'string'},
    {name: 'starred',         type: 'boolean'},
    {name: 'hidden',          type: 'boolean'},
    {name: 'enabled',         type: 'boolean'},
    {name: 'application_hives_id', type: 'int'},
    {name: 'application_hive_memberships_id', type: 'int'}
  ],
  belongsTo: "Membership"
});

/**
 * @class Kohive.models.Hive
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Hive", {
  // adapter: 'kohive',
  fields:  [
  	{name: 'id',                 type: 'int',    virtual: true},
  	{name: 'url_slug',           type: 'string', virtual: true},
  	{name: 'irc_channel_id',     type: 'string', virtual: true},
  	{name: 'type',               type: 'string'},
  	{name: 'title',              type: 'string'},
  	{name: 'invite_hashcode',    type: 'string'},
  	{name: 'short_description',  type: 'string'},
  	{name: 'background_image',   type: 'string'},
  	{name: 'top_toolbar_colour', type: 'string'},
		
  	{name: 'level', type: 'int'},
  	{
			name: 'type',
			virtual: true
		},

  	{name: 'creator_id',         type: 'string', virtual: true},
  	{name: 'owner_id',           type: 'string', virtual: true},
  	{name: 'creator_nickname',   type: 'string', virtual: true},
  	{name: 'owner_nickname',     type: 'string', virtual: true},
  	{name: 'creator_fullname',   type: 'string', virtual: true},
  	{name: 'owner_fullname',     type: 'string', virtual: true},
  	{name: 'member_count',       type: 'string', virtual: true},

  	{name: 'personal_hive_id',   type: 'int',    virtual: true},
    {name: 'access_level',       type: 'string', virtual: true}
  ],
  hasMany: ["Membership", "Milestone"]
  
  // saveWindowContext: function() {
  //   var windows = [];
  //   var manager = Kohive.OS.getWindowManager();
  //   
  //   manager.each(function(win) {
  //     windows.push(win);
  //     win.close();
  //   }, this);
  //   
  //   return windows;
  // },
  // 
  // restoreWindowContext: function(windows) {
  //   var windows = windows || [];
  //   
  //   for (var i=0; i < windows.length; i++) {
  //     windows[i].show();
  //   };
  // }
});

/**
 * @class Kohive.models.Membership
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Membership", {
  // adapter: 'kohive',
  fields:  [
    {name: 'id',                  type: 'int'},
    {name: 'id_card_id',          type: 'int'},
    {name: 'hive_id',             type: 'int'},
    {name: 'personal_hive_id',    type: 'int'},
    {name: 'access_level',        type: 'int'},
    {name: 'email',               type: 'string'},
    {name: 'state',               type: 'string'},
    {name: 'created_at',          type: 'string', virtual: true},
    {name: 'updated_at',          type: 'string', virtual: true},
		
    {name: 'email_notifications',        type: 'string'},
    {name: 'email_subscriptions_string', type: 'string'},
		
    {name: 'starred',  type: 'bool', virtual: true},
		
    {name: 'hive',    virtual: true},
    {name: 'id_card', virtual: true},

		{name: 'photo_album', virtual: true},
		{name: 'video_album', virtual: true},
    
    {name: 'sync_with_twitter',   type: 'bool'},
    
    {name: 'zoho_auth_token',   type: 'string'},
		
    {
      name: 'updated_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.updated_at, true);
      }
    },
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    }
  ],
  hasMany:   ["Hive" ,"Application", "Message"],
  
  // /**
  //  * Creates the Url for membership
  //  * @return {String} The Url
  //  */
  // url: function() {
  //   return '/memberships/' + this.data.id;
  // },
  
  /**
   * Creates the Url for accepting a membership
   * @return {String} The Url
   */
  getAcceptUrl: function() {
    return this.url() + '/accept';
  },
  
  /**
   * Creates the Url for rejecting a membership
   * @return {String} The Url
   */
  getRejectUrl: function() {
    return this.url() + '/reject';
  },
  
  /**
   * Creates the Url for cancelling a membership
   * @return {String} The Url
   */
  getCancelUrl: function() {
    return this.url() + '/cancel';
  },
  
  subscribedToEmail: function(eventClass) {
    var returnValue = false;
    // Get only first eventClass in case it's a single option for multiple events
    eventClass = eventClass.split(',')[0];
    Ext.each(this.get("email_subscriptions_string").split(','), function (eventCls) {
      if (eventCls == eventClass) {
        return returnValue = true;
      }
    });
    return returnValue || false;
  },
  
  subscribeToEmail: function(eventClass) {
    var returnValue = false,
        email_subscriptions = this.get("email_subscriptions_string") == '' ? [] : this.get("email_subscriptions_string").split(',');
    
    email_subscriptions.push(eventClass);
    this.set("email_subscriptions_string", email_subscriptions.join(','));
    return returnValue || false;
  },
  
  unsubscribeToEmail: function(eventClass) {
    var returnValue = false,
        email_subscriptions = [];
    Ext.each(this.get("email_subscriptions_string").split(','), function (eventCls) {
      if (eventCls != eventClass) {
        email_subscriptions.push(eventCls);
        returnValue = true;
      }
    });
    this.set("email_subscriptions_string", email_subscriptions.join(','));
    return returnValue || false;
  }
});

/**
 * @class Kohive.models.User
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.User", {
  //adapter:   'kohive',
  fields:    [
    {name: 'id',                    type: 'int', virtual: true},
    // {name: 'alias',              type: 'string'},
    // {name: 'language',           type: 'string'},
    // {name: 'timezone',           type: 'string'},
    {name: 'email',                 type: 'string'},
    {name: 'old_password',          type: 'string'},
    {name: 'password',              type: 'string'},
    {name: 'password_confirmation', type: 'string'},
    {name: 'default_hive_id',       type: 'string'},
    {name: 'default_id_card_id',    type: 'string'},
    {name: 'level',                 type: 'string'},
    {name: 'plan_switch_code',      type: 'string'},
    {name: 'shown_first_login',     type: 'bool'},
    {name: 'can_create_pro_hive',   type: 'bool', virtual: true},
    {name: 'is_online',             type: 'bool', virtual: true},
    
    {
      name: 'language',
      type: 'string',
      convert: function(value, record) {
        if (!value) return "en-GB";
        else return value;
      }
    },
    
    {
      name: 'renewal_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.renewal_date, true);
      }
    }
  ],
	
	/**
	 * Stores the amount of pro hives the user currently has
	 */
	proHivesCount: null,
	
	/**
	 * Counts the amount of pro hives the user has created
	 */
	countProHives: function() {
		//get the hvies
		var hives = Kohive.OS.getCurrentMemberships();
		if (!hives) return;
		
		//loop through each of the hives and check if any has been created by them + is a pro hive
		hives.each(function(hive) {
      // console.log(hive.data.hive);
		}, this);
	},
	
	/**
	 * Checks if the user can create another pro hive
	 */
	canCreateProHive: function() {
		return this.get('can_create_pro_hive');
	},
	
	/**
	 * 
	 */
	maxIdCards: function() {
		return (this.get('plan_switch_code').split('-')[0] != "free") ? 100 : 2;
	},
	
	getResendSignupEmailUrl: function() {
    return this.url() + '/resend_signup_email';
  }
});

/**
 * Attempts to load the currently logged in user
 * @param {Object} options Options object - pass a loadSuccess function to perform processing
 * @return {Ext.ajax.Request} The Ext.data.Request making the load call.  Usually not very useful,
 * add a loadSuccess and/or loadFailure function to the options object to gain access to the request results
 */
Kohive.models.User.loadFromSession = function(options) {
  var options = options || {};
  
  //FIXME: this is a bit of a hack really :-/ The id isn't actually used...
  Kohive.models.User.findById(1,
    Ext.applyIf(options, {
      url: '/account.json'
    })
  );
};

/**
 * @class Kohive.models.Contact
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Contact", {
  modelName: 'contact',
  jsonName:  'contact',
  urlName:   'contact',
	
  fields:    [
    {name: 'service',  type: 'string'},
    {name: 'name',     type: 'string', virtual: true},
    {name: 'login',    type: 'string'},
    {name: 'password', type: 'string'}
  ]
});

/**
 * @class Kohive.models.ApplicationHiveMemberships
 * 
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.ApplicationHiveMembership", {
  modelName: 'application_hive_membership',
  fields:    [
    {name: 'id',      type: 'int', virtual: true},
    {name: 'starred', type: 'string'}
  ]
});

/**
 * @class Kohive.models.Session
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Session", {
  modelName: 'session',
  fields:    [
    {name: '_kohive_session', type: 'string'},
    {name: 'auth_token',      type: 'string'}
  ]
});

ExtMVC.Model.define("Kohive.models.Message", {
  modelName: 'message',
  // jsonName:  'message',
  // urlName:   'messages',

  fields:  [
    {name: 'id',                     type: 'int',    virtual: true},
    {name: 'author_membership_id',   type: 'int'},
    {name: 'assignee_membership_id', type: 'int'},
    {name: 'hive_id',                type: 'int'},
    {name: 'body',                   type: 'string'},
    {name: 'read',                   type: 'bool'},
    {name: 'created_at',             type: 'string', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    {
      name:    'assignee_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.assignee_membership_id) return value;
        if (id == record.assignee_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        if (record.created_at) return Kohive.OS.convertToDate(record.created_at, true);
				else return false;
      }
    }
  ]
});

/**
 * @class Kohive.models.Milestone
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Milestone", {
  fields:  [
    {name: 'id',         type: 'int'},
    {name: 'hive_id',    type: 'int'},
    {name: 'title',      type: 'string'},
    {name: 'due_at',     type: 'string'},
    {name: 'created_at', type: 'string', virtual: true},
    {name: 'updated_at', type: 'string', virtual: true},
		
		{
      name: 'due_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.due_at, true);
      }
    },
    {
      name: 'updated_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.updated_at, true);
      }
    },
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    }
  ],
  belongsTo: ["Membership"]
});

ExtMVC.Model.define("Kohive.models.Update", {
  modelName: 'update',

  fields:  [
    {name: 'part', type: 'string'}
  ]
});

/**
 * @class Kohive.models.Album
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Album", {
  fields:    [
    {name: 'id',          type: 'int'},
    {name: 'author_membership_id', type: 'int'},
    {name: 'title',       type: 'string'},
    {name: 'description', type: 'string'},
    {name: 'hive_id',     type: 'int'},
    {name: 'type',        type: 'string'},
    {name: 'feed_url',    type: 'string'},
    {name: 'created_at',  type: 'string'},
    {name: 'updated_at',  type: 'string'},

		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  hasMany: "Image"
});

/**
 * @class Kohive.models.BoardPost
 * @extends Ext.ux.MVC.Model
 */
ExtMVC.Model.define("Kohive.models.BoardPost", {
  modelName: 'boardpost',
  jsonName:  'boardpost',
  urlName:   'board_posts',
  
  fields:    [
    {name: 'id',                      type: 'int', virtual: true},
    {name: 'body',                    type: 'string'},
    {name: 'body_html',               type: 'string', virtual: true},
    {name: 'recipient_membership_id', type: 'int'},
    {name: 'author_membership_id',    type: 'int'},
    {name: 'hive_id',                 type: 'int'},
    {name: 'created_at',              type: 'string'},
    
    {name: 'author_avatar',           type: 'string', virtual: true},
    {name: 'recipient_avatar',        type: 'string', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    {
      name:    'recipient_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.recipient_membership_id) return value;
        if (!record.recipient_membership_id) return Ext.ux.Translator.translate('everyone');
        if (id == record.recipient_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {name: 'toggled', type: 'boolean', virtual: true},
    
    // We need to use this in the template to check whether to show the reply button or not..
    {
      name: 'user_membership_id',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.getCurrentHive().get('id');
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },

		{
			name: 'tag_list',
			convert: function(value, record) {
				if (!value && record.tags.length > 0) {
					var res = "";
					Ext.each(record.tags, function(tag) {
					  res += tag.name + " ";
					}, this);
					return res;
				} else {
					return value;
				}
			}
		},
    {
      name: 'tags',
      virtual: true,
      convert: function(value, record) {
        if (value.length < 1) value = false;
        
        return value;
      }
    },
		
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isCustom(record, 'recipient_membership_id', 'id') || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  dataView: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
       '<tpl for=".">',
         '<div id="boardpost-{id}" class="x-pod x-boardpost<tpl if="can_edit"> x-editable-hover</tpl><tpl if="can_delete"> x-deletable-hover</tpl>">',
           '<div class="x-content-wrap">',
             '<div class="x-control-wrap"><div class="x-control"><div></div></div></div>',
             '<div class="x-content">',
              '<div class="x-title">',
                '<div class="x-title">{author_nickname}<tpl if="recipient_nickname"> <img class="x-spacer" src="/images/apps/right_arrow.png" /> {recipient_nickname}</tpl></div>',
                '<tpl if="toggled">',
                  '<div class="x-toggle-extra">',
                    '<tpl if="tags"><span class="x-tag"></span></tpl>',
                    '<tpl if="user_membership_id != author_membership_id && hive_id != 47"><span class="x-reply-btn action-reply-reply"></span></tpl>',
                  '</div>',
                '</tpl>',
                '<tpl if="!toggled">',
                  '<div class="x-toggle-extra">',
                    '<tpl if="user_membership_id != author_membership_id && hive_id != 47"><span class="x-reply-btn action-reply-reply"></span></tpl>',
                  '</div>',
                '</tpl>',
              '</div>',
                '<tpl if="!toggled">',
                  '<div class="x-extra">',
                    '<div class="x-message<tpl if="recipient_avatar"> recipient</tpl>">',
									
  								 		'<table cellpadding="0" width="100%">',
  											'<tr>',
  												'<td valign="top" nowrap="nowrap">',
  													'<div class="x-avatar">',
  			                      '<img src="{author_avatar}" />',
  			                      '<tpl if="recipient_avatar">',
  			                        '<img class="x-spacer" src="/images/apps/right_arrow_big.png" />',
  			                        '<img src="{recipient_avatar}" />',
  			                      '</tpl>',
  			                    '</div>',
  												'</td>',
  												'<td valign="top" width="100%">',
  													'<div class="action-edit-body">{body_html}</div>',
  												'</td>',
  											'</tr>',
  								 		'</table>',
								 		
                   '</div>',
  								 '<tpl if="tags">',
                     '<div class="x-tags">',
                       '<div class="action-editTags-tags">',
                         '<div class="x-form-field-wrap x-trigger-field-wrap">',
                           '<div class="x-form-text x-superboxselect  x-superboxselect-display-btns">',
                             '<ul>',
                               '<tpl for="tags">',
                                 '<li class="x-superboxselect-item no-close">{name}</li>',
                               '</tpl>',
                             '</ul>',
                           '</div>',
                         '</div>',
                       '</div>',
                     '</div>',
                   '</tpl>',
                   '<tpl if="!tags && can_edit">',
                     '<div class="x-tags x-empty"><div class="action-editTags-tags">' + Ext.ux.Translator.translate('app_add_tags') + '</div></div>',
                   '</tpl>',
                   '<div class="x-other">',
                     '<div class="x-created-on">' + '{created_at_date}</div>',
                   '</div>',
                 '</div>',
               '</tpl>',
             '</div>',
             '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
           '</div>',
         '</div>',
       '</tpl>',
     '</div>'
  ),
  
  clickActions: [
    {
      name:         'body',
			value:        'body',
      type:         'edit',
      fieldType:    'textarea',
      method:       'dblclick',
      forceHeight:  true,
      forceRefresh: true,
      emptyText:    Ext.ux.Translator.translate('app_boardposts_add_description')
    },
    {
      name:   'tags',
      type:   'editTags',
      method: 'dblclick'
    },
    {
      name:            'reply',
      type:            'reply',
      method:          'click',
      needsPermission: false
    }
  ],
  
  /**
   * Method which attempts to reply to the records author
   */
  reply: function(dataview, record, el, a, index, indexWindow) {
    // Get the records author_membership_id
    var author_membership_id = record.get('author_membership_id');
    var tags                 = record.get('tags') || '';
    
		record.data.tag_list = "";
		Ext.each(tags, function(tag) {
			if (!tag.name) return;
			record.data.tag_list = record.get('tag_list') + ' ' + tag.name;
		}, this);
		
		// trim space at beginning, fixes bug that tag field stays empty
    record.data.tag_list = record.data.tag_list.trim();
		
    // Check if the newForm is there in the controller
    if (!indexWindow.newForm || !author_membership_id) return;
    
    // select the proper person in the combobox
    indexWindow.newForm.comboBox.setValue(author_membership_id);
    indexWindow.newForm.tagsField.setValue(record.get('tag_list'));
    if (indexWindow.newForm.collapsed) indexWindow.toggleNewForm(true);
    
    // Scroll to the top of the view
    Ext.get(indexWindow.el.select('.x-panel-body').elements[2].id).scroll('t', 30000, true);
    
    var tempTask = new Ext.util.DelayedTask(function() {
      // Focus on the body field
      Ext.get('board-posts-body' + Kohive.OS.getCurrentHiveId()).focus();
    }, this);
    tempTask.delay(1000);
  },
  
  /**
	 * 
	 */
	editTags: function(dataview, record, el, a, index, indexWindow) {
	  var sibling = el.insertSibling({
			id:  'edit-tags-field'
		});
		
		el.enableDisplayMode();
		el.hide();
	  
	  this.tagsField = new Ext.ux.form.SuperBoxSelect({
	    renderTo:        'edit-tags-field',
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      store:           indexWindow.tagsStore,
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        newitem: function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        },
        expand: function() {
          var list = this.tagsField.list;
          
          var tempTask = new Ext.util.DelayedTask(function() {
            list.setTop(list.getTop() + 3);
          }, this);
          tempTask.delay(200);
        },
        'enter-key': function(field) {
          this.saveTags(field, record, sibling, el);
        },
        blur: function(field) {
          this.saveTags(field, record, sibling, el);
				}
      }
		});
		
		this.tagsField.setWidth(sibling.getWidth());
    
    //format tags
    var tags = Ext.util.Format.trim(record.get('tag_list')).split(' ');
		
		var array = [];
		Ext.each(tags, function(t) {
		  array.push({
		    tag: t
		  });
		}, this);
		
		//set the tags value
    this.tagsField.setValueEx(array);
    
    this.tagsField.focus();
	},
	
	saveTags: function(field, record, sibling, el, dataview) {
		var val = Ext.util.Format.trim(this.tagsField.getValue()).split(',');
		
		if (val.length > 0) {
		  if (val.length == 1 && val[0] == "") {
		    record.set('tag_list', '');
  			record.set('tags', false);
  			this.tagsField.setValue('');
		  } else {
  		  var newArray = [];
		  
  		  Ext.each(val, function(a) {
  		    newArray.push({name:a});
  		  }, this);
		  
  			record.set('tag_list', val.join(' '));
  			record.set('tags', false);
  			record.set('tags', newArray);
  			this.tagsField.setValue(val);
  		};
		};
		
		this.showUpdating();
		
		record.save({
			url:   '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/board_posts/' + record.get('id'),
			scope: this,
			success: function() {
				this.tagsField.destroy();
				el.show();
				this.hideLoading();
			},
			failure: Ext.emptyFn
		});
	},
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('boardposts');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	/**
	 * Hides the loading animation from the view
	 */
	hideLoading: function() {
	  var controller = Kohive.OS.getController('boardposts');
		var view = controller.indexWindow || null;
		
		if (view) {
		  view.hideLoading();
		  view.fireEvent('update-filter-bar', view.cachedStore);
		};
	},
	
	/**
	 * Refreshes the content in the view
	 */
	refreshView: function() {
		var controller = Kohive.OS.getController('boardposts');
		var view = controller.indexWindow || null;
		
    if (view && view.store) view.store.load();
		if (view) view.hideLoading();
	}
});

/**
 * @class Kohive.models.Blog
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Blog", {
  modelName: 'blog',
  jsonName:  'blog',
  urlName:   'blog',
	
  fields:    [
    {name: 'id',                   type: 'int', virtual: true},
    {name: 'hive_id',              type: 'int', virtual: true},
    {name: 'title',                type: 'string'},
    {name: 'body',                 type: 'string'},
    {name: 'body_html',            type: 'string'},
    {name: 'author_membership_id', type: 'int'},
    {name: 'created_at',           type: 'string', virtual: true},
    {name: 'modified_at',          type: 'string', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {name: 'toggled', type: 'boolean', virtual: true},
    
    //comments
    {name: 'toggled_comments', type: 'boolean', virtual: true},
    {
      name   : 'comments',
      virtual: true,
      convert: function(value, record) {
        if (value) return value;
        else [];
      }
    },
    {name: 'new_comment',   type: 'int',     virtual: true},
    {name: 'hide_comments', type: 'boolean', virtual: true},
    
    {
			name: 'tag_list',
			convert: function(value, record) {
				if (!value && record.tags.length > 0) {
					var res = "";
					Ext.each(record.tags, function(tag) {
					  res += tag.name + " ";
					}, this);
					return res;
				} else {
					return value;
				}
			}
		},
    {
      name: 'tags',
      virtual: true,
      convert: function(value, record) {
        if (value.length < 1) value = false;
        
        return value;
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
		
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  dataView: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
       '<tpl for=".">',
         '<div id="blog-{id}" class="x-pod x-blog<tpl if="can_edit"> x-editable-hover</tpl><tpl if="can_delete"> x-deletable-hover</tpl>">',
           '<div class="x-content-wrap">',
             '<div class="x-control-wrap"><div class="x-control"><div></div></div></div>',
             '<div class="x-content">',
               '<div class="x-title">',
                 '<div class="x-title action-edit-title">{title}</div>',
                 '<tpl if="toggled">',
                   '<div class="x-toggle-extra">',
                     '<tpl if="tags"><span class="x-tag"></span></tpl>',
                   '</div>',
                 '</tpl>',
               '</div>',
               '<tpl if="!toggled">',
                 '<div class="x-extra">',
                   '<tpl if="body_html">',
                     // '<div class="x-message"><div>{body_html}</div></div>',
                     '<div class="x-message"><div class="action-edit-body">{body_html}</div></div>',
                   '</tpl>',
                   '<tpl if="tags">',
                     '<div class="x-tags">',
                       '<div class="action-editTags-tags">',
                         '<div class="x-form-field-wrap x-trigger-field-wrap">',
                           '<div class="x-form-text x-superboxselect  x-superboxselect-display-btns">',
                             '<ul>',
                               '<tpl for="tags">',
                                 '<li class="x-superboxselect-item no-close">{name}</li>',
                               '</tpl>',
                             '</ul>',
                           '</div>',
                         '</div>',
                       '</div>',
                     '</div>',
                   '</tpl>',
                   '<tpl if="!tags && can_edit">',
                     '<div class="x-tags x-empty"><div class="action-editTags-tags">' + Ext.ux.Translator.translate('app_add_tags') + '</div></div>',
                   '</tpl>',
                   '<div class="x-other">',
                     '<div class="x-created-on">' + '{author_nickname}, {created_at_date}</div>',
                     //'<div class="x-created-on">' + Ext.ux.Translator.translate('created_on') + ': {created_at_date}</div>',
                   '</div>',
                 '</div>',
               '</tpl>',
             '</div>',
             '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
           '</div>',
           
           '<tpl if="!toggled">',
              '<div class="x-comments<tpl if="this.isSmall(comments, toggled_comments, new_comment)"> x-small</tpl><tpl if="this.hasComments(comments)"> x-hascomments</tpl><tpl if="new_comment"> x-adding-comment</tpl>">',
                '<tpl if="!toggled_comments">',
                  '<tpl for="comments">',
                    '<div class="x-comment" id="blog-comment-{#}-{id}">',
                      '<div class="x-delete-comment"><div></div></div>',
                      '<div class="x-author"><img src="{thumb_filename}" /> {[Kohive.OS.isAuthor(values) ? Ext.ux.Translator.translate("me") : values.author_nickname]}</div>',
                      '<div class="x-body">{content}</div>',
                      '<div class="x-date">{[(values.created_at.split("T")[1]) ? Kohive.OS.convertToDate(values.created_at, true) : values.created_at]}</div>',
                    '</div>',
                  '</tpl>',
                '</tpl>',
                '<tpl if="!this.isSmall(comments, toggled_comments, new_comment)">',
                  '<div class="x-new-comment<tpl if="this.hasComments(comments)"> x-extended</tpl><tpl if="new_comment"> x-is-field</tpl>">',
                    '<tpl if="!new_comment && this.hasComments(comments)"><div class="x-toggle-button"><div class="x-img"></div></div></tpl>',
                    '<tpl if="!new_comment"><div class="x-button"><div class="x-img"></div></div></tpl>',
                    '<tpl if="new_comment">',
                      '<div class="x-field"><input type="text" /></div>',
                      '<div class="x-submit"><span>' + Ext.ux.Translator.translate('post') + '</span></div>',
                    '</tpl>',
                  '</div>',
                '</tpl>',
                '<tpl if="this.isSmall(comments, toggled_comments, new_comment)">',
                  '<div class="x-new-comment x-small">',
                    '<tpl if="this.hasComments(comments)"><div class="x-toggle-button toggled"><div class="x-img"></div><span class="x-txt">{[this.commentsLength(values.comments)]}</span></div></tpl>',
                    '<tpl if="!this.hasComments(comments)"><div class="x-button"><div class="x-img"></div><span class="x-txt">{[Ext.ux.Translator.translate("new_comment")]}</span></div></tpl>',
                  '</div>',
                '</tpl>',
              '</div>',
            '</tpl>',

            {
              isSmall: function(comments, toggled_comments, new_comment) {
                if (new_comment) return false;
                if (comments.length == 0 && toggled_comments) return true;
                if (comments.length == 0 && !toggled_comments) return true;
                if (comments.length > 0 && toggled_comments) return true;
                return false;
              },
              hasComments: function(comments) {
                return comments.length > 0;
              },
              commentsLength: function(comments) {
                var length = comments.length;
                if (comments.length == 0) return length + " " + Ext.ux.Translator.translate('comments');
                if (comments.length == 1) return length + " " + Ext.ux.Translator.translate('comment');
                if (comments.length > 1) return length + " " + Ext.ux.Translator.translate('comments');
              }
            },
           
         '</div>',
       '</tpl>',
     '</div>'
  ),
  
  clickActions: [
    {
      name:       'title',
      type:       'edit',
      method:     'dblclick',
      allowBlank: false
    },
    {
      name:         'body',
			value:        'body',
      type:         'edit',
      fieldType:    'textarea',
			forceRefresh: true,
      method:       'dblclick',
      forceHeight: true
    },
    {
      name:   'tags',
      type:   'editTags',
      method: 'dblclick'
    }
  ],
	
	/**
	 * 
	 */
  // updateBody: function(dataview, record, el, a, index) {
  //  //create an element to store the tinymce instanse
  //  var id     = Ext.id(),
  //         bodyEl = el.parent().createChild({
  //    id:  id,
  //    cls: 'x-tinymce-ediable'
  //  });
  //  
  //  //hide the original el
  //  el.enableDisplayMode();
  //  el.hide();
  //  
  //  //make the tinymce instance
  //  tinyMCE.execCommand("mceAddControl", true, id);
  //  
  //   //update content
  //  var tempTask = new Ext.util.DelayedTask(function() {
  //    var instance = tinyMCE.getInstanceById(id);
  //    instance.setContent(record.get('body_html'));
  //    
  //    //add a listener to the body
  //    Ext.getBody().on('mousedown', function(e) {
  //      var targetEl = Ext.get(e.getTarget());
  // 
  //      var isBody = false;
  //        
  //      //check if it is the body you click
  //      if (targetEl.parent() && targetEl.parent().hasClass("mceToolbar")) isBody = true;
  //      if (targetEl.parent() && targetEl.parent().hasClass("x-message")) isBody = true;
  //      if (targetEl.parent().parent() && targetEl.parent().parent().hasClass("mceToolbar")) isBody = true;
  //      if (targetEl.parent().parent() && targetEl.parent().parent().hasClass("x-message")) isBody = true;
  //      if (targetEl.parent().parent().parent() && targetEl.parent().parent().parent().hasClass("mceToolbar")) isBody = true;
  //      if (targetEl.parent().parent().parent() && targetEl.parent().parent().parent().hasClass("x-message")) isBody = true;
  //      if (targetEl.parent().parent().parent().parent() && targetEl.parent().parent().parent().parent().hasClass("mceToolbar")) isBody = true;
  //      if (targetEl.parent().parent().parent().parent() && targetEl.parent().parent().parent().parent().hasClass("x-message")) isBody = true;
  //      if (targetEl.parent().parent().parent().parent().parent() && targetEl.parent().parent().parent().parent().parent().hasClass("mceToolbar")) isBody = true;
  //      if (targetEl.parent().parent().parent().parent().parent() && targetEl.parent().parent().parent().parent().parent().hasClass("x-message")) isBody = true;
  //      if (targetEl.parent().parent().parent().parent().parent() && targetEl.parent().parent().parent().parent().parent().parent().hasClass("mceToolbar")) isBody = true;
  //      if (targetEl.parent().parent().parent().parent().parent() && targetEl.parent().parent().parent().parent().parent().parent().hasClass("x-message")) isBody = true;
  //      
  //      if (!isBody) {
  //        var content = instance.getContent();
  //        
  //        this.showUpdating();
  //        
  //        instance.remove();
  //        bodyEl.remove();
  //        
  //        //save and end
  //        record.set('body_html', content);
  //        
  //        // Update the record with the new state value
  //        record.save({
  //          url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/blog/' + record.get('id'),
  //          success: this.refreshView,
  //          failure: function() {
  //            return Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
  //          }
  //        });
  //      };
  //    }, this);
  //  }, this);
  //  tempTask.delay(500);
  //   },
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('blogs');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	refreshView: function() {
		var controller = Kohive.OS.getController('blogs');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.store.load();
		if (view && view.store) view.showRefreshing();
	},
	
	/**
	 * 
	 */
	editTags: function(dataview, record, el, a, index, indexWindow) {
	  var sibling = el.insertSibling({
			id:  'edit-tags-field'
		});
		
		el.enableDisplayMode();
		el.hide();
	  
	  this.tagsField = new Ext.ux.form.SuperBoxSelect({
	    renderTo:        'edit-tags-field',
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      store:           indexWindow.tagsStore,
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        newitem: function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        },
        expand: function() {
          var list = this.tagsField.list;
          
          var tempTask = new Ext.util.DelayedTask(function() {
            list.setTop(list.getTop() + 3);
          }, this);
          tempTask.delay(200);
        },
        'enter-key': function(field) {
          this.saveTags(field, record, sibling, el);
        },
        blur: function(field) {
          this.saveTags(field, record, sibling, el);
				}
      }
		});
		
		this.tagsField.setWidth(sibling.getWidth());
    
    //format tags
    var tags = Ext.util.Format.trim(record.get('tag_list')).split(' ');
		
		var array = [];
		Ext.each(tags, function(t) {
		  array.push({
		    tag: t
		  });
		}, this);
		
		//set the tags value
    this.tagsField.setValueEx(array);
    
    this.tagsField.focus();
	},
	
	saveTags: function(field, record, sibling, el, dataview) {
		var val = Ext.util.Format.trim(this.tagsField.getValue()).split(',');
		
		if (val.length > 0) {
		  if (val.length == 1 && val[0] == "") {
		    record.set('tag_list', '');
  			record.set('tags', false);
  			this.tagsField.setValue('');
		  } else {
  		  var newArray = [];
		  
  		  Ext.each(val, function(a) {
  		    newArray.push({name:a});
  		  }, this);
		  
  			record.set('tag_list', val.join(' '));
  			record.set('tags', false);
  			record.set('tags', newArray);
  			this.tagsField.setValue(val);
  		};
		};
		
		this.showUpdating();
		
		record.save({
			url:   '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/blogs/' + record.get('id'),
			scope: this,
			success: function() {
				this.tagsField.destroy();
				el.show();
				this.hideLoading();
			},
			failure: Ext.emptyFn
		});
	},
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('blogs');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	/**
	 * Hides the loading animation from the view
	 */
	hideLoading: function() {
	  var controller = Kohive.OS.getController('blogs');
		var view = controller.indexWindow || null;
		
		if (view) {
		  view.hideLoading();
		  view.fireEvent('update-filter-bar', view.cachedStore);
		};
	},
	
	/**
	 * Refreshes the content in the view
	 */
	refreshView: function() {
		var controller = Kohive.OS.getController('blogs');
		var view = controller.indexWindow || null;
		
    if (view && view.store) view.store.load();
		if (view) view.hideLoading();
	}
});

/**
 * @class Kohive.models.Bookmark
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Bookmark", {
  modelName: 'bookmark',
  fields:    [
    {name: 'id',                   type: 'int', virtual: true},
    {name: 'hive_id',              type: 'int', virtual: true},
    {name: 'title',                type: 'string'},
    {name: 'url',                  type: 'string'},
    {name: 'note',                 type: 'string'},
    {name: 'notes_html',           type: 'string'},
    {name: 'author_membership_id', type: 'int'},
    {name: 'created_at',           type: 'string', virtual: true},
    {name: 'modified_at',          type: 'string', virtual: true},
    {name: 'toggled',              type: 'boolean', virtual: true},
    
    //comments
    {name: 'toggled_comments', type: 'boolean', virtual: true},
    {
      name   : 'comments',
      virtual: true,
      convert: function(value, record) {
        if (value) return value;
        else [];
      }
    },
    {name: 'new_comment',   type: 'int',     virtual: true},
    {name: 'hide_comments', type: 'boolean', virtual: true},
    
    //convert fields
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
    
    //tags
    {
			name: 'tag_list',
			convert: function(value, record) {
				if (!value && record.tags.length > 0) {
					var res = "";
					Ext.each(record.tags, function(tag) {
					  res += tag.name + " ";
					}, this);
					return res;
				} else {
					return value;
				}
			}
		},
    {
      name: 'tags',
      virtual: true,
      convert: function(value, record) {
        if (value.length < 1) value = false;
        
        return value;
      }
    },
		
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  dataView: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
      '<tpl for=".">',
        '<div id="bookmark-{id}" class="x-pod x-bookmark<tpl if="can_edit"> x-editable-hover</tpl><tpl if="can_delete"> x-deletable-hover</tpl>">',
          '<div class="x-content-wrap">',
            '<div class="x-control-wrap"><div class="x-control"><div></div></div></div>',
            '<div class="x-content">',
              '<div class="x-title">',
                '<div class="x-title action-edit-title">{title}</div>',
                '<tpl if="toggled">',
                  '<div class="x-toggle-extra">',
                    '<tpl if="tags"><span class="x-tag"></span></tpl>',
                    '<tpl if="note"><span class="x-note"></span></tpl>',
                    '<span class="x-url action-click-goto"></span>',
                  '</div>',
                '</tpl>',
              '</div>',
              '<tpl if="!toggled">',
                '<div class="x-extra">',
                  '<div class="x-url"><div class="x-go-btn action-click-goto"></div><div class="action-edit-url">{url}</div></div>',
                   '<tpl if="note">',
                     '<div class="x-description"><div class="action-edit-note">{notes_html}</div></div>',
                   '</tpl>',
                   '<tpl if="!note && can_edit">',
                     '<div class="x-description x-empty"><div class="action-edit-note">' + Ext.ux.Translator.translate('app_add_note') + '</div></div>',
                   '</tpl>',
                   '<tpl if="tags">',
                     '<div class="x-tags">',
                       '<div class="action-editTags-tags">',
                         '<div class="x-form-field-wrap x-trigger-field-wrap">',
                           '<div class="x-form-text x-superboxselect  x-superboxselect-display-btns">',
                             '<ul>',
                               '<tpl for="tags">',
                                 '<li class="x-superboxselect-item no-close">{name}</li>',
                               '</tpl>',
                             '</ul>',
                           '</div>',
                         '</div>',
                       '</div>',
                     '</div>',
                   '</tpl>',
                   '<tpl if="!tags && can_edit">',
                     '<div class="x-tags x-empty"><div class="action-editTags-tags">' + Ext.ux.Translator.translate('app_add_tags') + '</div></div>',
                   '</tpl>',
                   '<div class="x-other">',
                     '<div class="x-created-on">' + '{author_nickname}, {created_at_date}</div>',
                     //'<div class="x-created-on">' + Ext.ux.Translator.translate('created_on') + ': {created_at_date}</div>',
                   '</div>',
                 '</div>',
               '</tpl>',
             '</div>',
             '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
           '</div>',
           
           '<tpl if="!toggled">',
             '<div class="x-comments<tpl if="this.isSmall(comments, toggled_comments, new_comment)"> x-small</tpl><tpl if="this.hasComments(comments)"> x-hascomments</tpl><tpl if="new_comment"> x-adding-comment</tpl>">',
               '<tpl if="!toggled_comments">',
                 '<tpl for="comments">',
                   '<div class="x-comment" id="bookmark-comment-{#}-{id}">',
                     '<div class="x-delete-comment"><div></div></div>',
                     '<div class="x-author"><img src="{thumb_filename}" /> {[Kohive.OS.isAuthor(values) ? Ext.ux.Translator.translate("me") : values.author_nickname]}</div>',
                     '<div class="x-body">{content}</div>',
                     '<div class="x-date">{[(values.created_at.split("T")[1]) ? Kohive.OS.convertToDate(values.created_at, true) : values.created_at]}</div>',
                   '</div>',
                 '</tpl>',
               '</tpl>',
               '<tpl if="!this.isSmall(comments, toggled_comments, new_comment)">',
                 '<div class="x-new-comment<tpl if="this.hasComments(comments)"> x-extended</tpl><tpl if="new_comment"> x-is-field</tpl>">',
                   '<tpl if="!new_comment && this.hasComments(comments)"><div class="x-toggle-button"><div class="x-img"></div></div></tpl>',
                   '<tpl if="!new_comment"><div class="x-button"><div class="x-img"></div></div></tpl>',
                   '<tpl if="new_comment">',
                     '<div class="x-field"><input type="text" /></div>',
                     '<div class="x-submit"><span>' + Ext.ux.Translator.translate('post') + '</span></div>',
                   '</tpl>',
                 '</div>',
               '</tpl>',
               '<tpl if="this.isSmall(comments, toggled_comments, new_comment)">',
                 '<div class="x-new-comment x-small">',
                   '<tpl if="this.hasComments(comments)"><div class="x-toggle-button toggled"><div class="x-img"></div><span class="x-txt">{[this.commentsLength(values.comments)]}</span></div></tpl>',
                   '<tpl if="!this.hasComments(comments)"><div class="x-button"><div class="x-img"></div><span class="x-txt">{[Ext.ux.Translator.translate("new_comment")]}</span></div></tpl>',
                 '</div>',
               '</tpl>',
             '</div>',
           '</tpl>',
           
           {
             isSmall: function(comments, toggled_comments, new_comment) {
               if (new_comment) return false;
               if (comments.length == 0 && toggled_comments) return true;
               if (comments.length == 0 && !toggled_comments) return true;
               if (comments.length > 0 && toggled_comments) return true;
               return false;
             },
             hasComments: function(comments) {
               return comments.length > 0;
             },
             commentsLength: function(comments) {
               var length = comments.length;
               if (comments.length == 0) return length + " " + Ext.ux.Translator.translate('comments');
               if (comments.length == 1) return length + " " + Ext.ux.Translator.translate('comment');
               if (comments.length > 1) return length + " " + Ext.ux.Translator.translate('comments');
             }
           },
           
         '</div>',
       '</tpl>',
     '</div>'
  ),
  
  clickActions: [
    {
      name:       'title',
      type:       'edit',
      method:     'dblclick',
      allowBlank: false
    },
    {
      name:       'url',
      type:       'edit',
      method:     'dblclick',
      allowBlank: false
    },
    {
      name:         'note',
			value:        'note',
      type:         'edit',
      fieldType:    'textarea',
      method:       'dblclick',
      emptyText:    Ext.ux.Translator.translate('app_add_note'),
			forceRefresh: true
    },
    {
      name:   'tags',
      type:   'editTags',
      method: 'dblclick'
    },
    {
      name:      'goto',
      fieldName: 'url',
      type:      'click',
      method:    'click'
    }
  ],
  
  /**
	 * 
	 */
	editTags: function(dataview, record, el, a, index, indexWindow) {
	  var sibling = el.insertSibling({
			id:  'edit-tags-field'
		});
		
		el.enableDisplayMode();
		el.hide();
	  
	  this.tagsField = new Ext.ux.form.SuperBoxSelect({
	    renderTo:        'edit-tags-field',
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      store:           indexWindow.tagsStore,
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        newitem: function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        },
        expand: function() {
          var list = this.tagsField.list;
          
          var tempTask = new Ext.util.DelayedTask(function() {
            list.setTop(list.getTop() + 3);
          }, this);
          tempTask.delay(200);
        },
        'enter-key': function(field) {
          this.saveTags(field, record, sibling, el);
        },
        blur: function(field) {
          this.saveTags(field, record, sibling, el);
				}
      }
		});
		
		this.tagsField.setWidth(sibling.getWidth());
    
    //format tags
    var tags = Ext.util.Format.trim(record.get('tag_list')).split(' ');
		
		var array = [];
		Ext.each(tags, function(t) {
		  array.push({
		    tag: t
		  });
		}, this);
		
		//set the tags value
    this.tagsField.setValueEx(array);
    
    this.tagsField.focus();
	},
	
	saveTags: function(field, record, sibling, el, dataview) {
		var val = Ext.util.Format.trim(this.tagsField.getValue()).split(',');
		
		if (val.length > 0) {
		  if (val.length == 1 && val[0] == "") {
		    record.set('tag_list', '');
  			record.set('tags', false);
  			this.tagsField.setValue('');
		  } else {
  		  var newArray = [];
		  
  		  Ext.each(val, function(a) {
  		    newArray.push({name:a});
  		  }, this);
		  
  			record.set('tag_list', val.join(' '));
  			record.set('tags', false);
  			record.set('tags', newArray);
  			this.tagsField.setValue(val);
  		};
		};
		
		this.showUpdating();
		
		record.save({
			url:   '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/bookmarks/' + record.get('id'),
			scope: this,
			success: function() {
				this.tagsField.destroy();
				el.show();
				this.hideLoading();
			},
			failure: Ext.emptyFn
		});
	},
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('bookmarks');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	/**
	 * Hides the loading animation from the view
	 */
	hideLoading: function() {
	  var controller = Kohive.OS.getController('bookmarks');
		var view = controller.indexWindow || null;
		
		if (view) {
		  view.hideLoading();
		  view.fireEvent('update-filter-bar', view.cachedStore);
		};
	},
	
	/**
	 * Refreshes the content in the view
	 */
	refreshView: function() {
		var controller = Kohive.OS.getController('bookmarks');
		var view = controller.indexWindow || null;
		
    if (view && view.store) view.store.load();
		if (view) view.hideLoading();
	}
});

/**
 * @class Kohive.models.Comment
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Comment", {
  fields:    [
    {name: 'id',                   type: 'int'   },
    {name: 'other_id',             type: 'int', virtual: true},
    {name: 'title',                type: 'string'},
    {name: 'content',              type: 'string'},
    {name: 'author_membership_id', type: 'int'   },
    {name: 'thumb_filename',       type: 'string', virtual: true},
    {name: 'created_at',           type: 'string', virtual: true},
    {name: 'updated_at',           type: 'string'},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
		
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  belongsTo: ["Image", "Video", "Task"]
});

/**
 * @class Kohive.models.DesktopSticky
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.DesktopSticky", {
  modelName: 'desktop_sticky',
  fields:    [
    {name: 'id',                   type: 'int',    virtual: true},
    {name: 'hive_id',              type: 'int'                  },
    {name: 'sticky_id',            type: 'int'                  },
    {name: 'author_membership_id', type: 'int'                  },
    {name: 'created_at',           type: 'string', virtual: true},
    {name: 'updated_at',           type: 'string', virtual: true},
    
    {name: 'the_sticky', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.the_sticky.author_membership_id) return record.the_sticky.author_nickname;
        if (id == record.the_sticky.author_membership_id) return Ext.ux.Translator.translate('me');
        return record.the_sticky.author_nickname;
      }
    },
    
    {
      name:    'color_cls',
      virtual: true,
      convert: function(value, record) {
        return record.the_sticky.color + (Math.floor(Math.random() * 3) + 1);
      }
    },
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.the_sticky.created_at, true);
      }
    },
   	
		//editable properties
    {
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  dataView: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
      '<tpl for=".">',
        '<div id="desktop-sticky-{id}" class="x-sticky {values.the_sticky.color} {color_cls} x-pod<tpl if="can_delete"> x-deletable-hover</tpl>">',
          '<div class="x-stamp<tpl if="values.the_sticky.stamp"> {values.the_sticky.stamp}</tpl>">',
            '<div class="x-created">{author_nickname}, {created_at_date}</div>',
            '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
          '</div>',
          '<div id="middle" class="x-sticky-wrap">',
            '<div id="inner" class="x-sticky-inner-wrap">',
              '<div class="x-body action-edit-body">{values.the_sticky.body_html}</div>',
            '</div>',
          '</div>',
        '</div>',
      '</tpl>',
    '</div>'
  )
});

/**
 * @class Kohive.models.File
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.File", {
	modelName: 'file',
	urlName:   'file',
	jsonName:  'file',
  fields:    [
    {name: 'id',                   type: 'int', virtual: true},
    {name: 'title',                type: 'string'},
    {name: 'author_membership_id', type: 'int'},
    {name: 'file_file_name',       type: 'string'},
    {name: 'file_file_size',       type: 'int', virtual: true},
    {name: 'url',                  type: 'string', virtual: true},
    {name: 'extension',            type: 'string', virtual: true},
    {name: 'file_extension',       type: 'string'},
    {name: 'description',          type: 'string'},
    {name: 'description_html',     type: 'string'},
    
    {
      name: 'versions',
      virtual: true,
      convert: function(value, record) {
        var array = [],
            value = value || [],
            i     = 1;
        
        Ext.each(value, function(version, index) {
          if (index == value.length - 1) return;
          
          //date
          version.created_at = Kohive.OS.convertToDate(version.created_at, true);
          
          //file size
          var file_file_size = version.file_file_size,
              newValue       = 0,
              type           = "mb";
					newValue = Math.round(file_file_size / 1048576 * 100) / 100;
					if (/^0.*/.test(newValue)) {
						newValue = Math.round(file_file_size / 1024);
						type = "kb";
					};
					version.file_file_size = newValue + " " + type;
					
					//version id
					version.num = i;
					i = i + 1;
					
          array.push(version);
        }, this);
        
        return (array.length > 0) ? array.reverse() : false;
      }
    },
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {name: 'toggled', type: 'boolean', virtual: true},
    
    {
			name: 'tag_list',
			convert: function(value, record) {
				if (!value && record.tags.length > 0) {
					var res = "";
					Ext.each(record.tags, function(tag) {
					  res += tag.name + " ";
					}, this);
					return res;
				} else {
					return value;
				}
			}
		},
    {
      name: 'tags',
      virtual: true,
      convert: function(value, record) {
        if (value.length < 1) value = false;
        
        return value;
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at);
      }
    },
    {
      name: 'id_card',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.getIdCardFromMembershipId(record.author_membership_id);
      }
    },
    
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],

	clickActions: [
    {
      name:       'title',
      type:       'edit',
      method:     'dblclick',
      allowBlank: false
    },
    // {
    //   name:       'url',
    //   type:       'edit',
    //   method:     'dblclick',
    //   allowBlank: false
    // },
    {
      name:         'description',
			value:        'description',
      type:         'edit',
      fieldType:    'textarea',
      method:       'dblclick',
      emptyText:    Ext.ux.Translator.translate('app_add_description'),
			forceRefresh: true
    },
    {
      name:   'tags',
      type:   'editTags',
      method: 'dblclick',
			forceRefresh: true
    }
  ],
  
  // belongsTo: "Album",
  hasMany:   "Comment",
  
  /**
	 * 
	 */
	editTags: function(dataview, record, el, a, index, indexWindow) {
	  var sibling = el.insertSibling({
			id:  'edit-tags-field'
		});
		
		el.enableDisplayMode();
		el.hide();
	  
	  this.tagsField = new Ext.ux.form.SuperBoxSelect({
	    renderTo:        'edit-tags-field',
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      store:           indexWindow.tagsStore,
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        newitem: function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        },
        expand: function() {
          var list = this.tagsField.list;
          
          var tempTask = new Ext.util.DelayedTask(function() {
            list.setTop(list.getTop() + 3);
          }, this);
          tempTask.delay(200);
        },
        'enter-key': function(field) {
          this.saveTags(field, record, sibling, el);
        },
        blur: function(field) {
          this.saveTags(field, record, sibling, el);
				}
      }
		});
		
		this.tagsField.setWidth(sibling.getWidth());
    
    //format tags
    var tags = Ext.util.Format.trim(record.get('tag_list')).split(' ');
		
		var array = [];
		Ext.each(tags, function(t) {
		  array.push({
		    tag: t
		  });
		}, this);
		
		//set the tags value
    this.tagsField.setValueEx(array);
    
    this.tagsField.focus();
	},
	
	saveTags: function(field, record, sibling, el, dataview) {
		var val = Ext.util.Format.trim(this.tagsField.getValue()).split(',');
		
		if (val.length > 0) {
		  if (val.length == 1 && val[0] == "") {
		    record.set('tag_list', '');
  			record.set('tags', false);
  			this.tagsField.setValue('');
		  } else {
  		  var newArray = [];
		  
  		  Ext.each(val, function(a) {
  		    newArray.push({name:a});
  		  }, this);
		  
  			record.set('tag_list', val.join(' '));
  			record.set('tags', false);
  			record.set('tags', newArray);
  			this.tagsField.setValue(val);
  		};
		};
		
		this.showUpdating();
		
		record.save({
			url:   '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/file_cabinets/' + record.get('id'),
			scope: this,
			success: function() {
				this.tagsField.destroy();
				el.show();
				this.hideLoading();
				this.refreshView();
			},
			failure: Ext.emptyFn
		});
	},
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('filecabinet');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	/**
	 * Hides the loading animation from the view
	 */
	hideLoading: function() {
	  var controller = Kohive.OS.getController('filecabinet');
		var view = controller.indexWindow || null;
		
		if (view) {
		  view.hideLoading();
		  view.fireEvent('update-filter-bar', view.cachedStore);
		};
	},
	
	/**
	 * Refreshes the content in the view
	 */
	refreshView: function() {
		var controller = Kohive.OS.getController('filecabinet');
		var view = controller.indexWindow || null;
		
    if (view && view.store) view.store.load();
		if (view) view.hideLoading();
	}
});

/**
 * @class Kohive.models.IdCard
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.IdCard", {
  modelName: 'id_card',
  fields:    [
    {name: 'id',                     type: 'int'},
    {name: 'user_id',                type: 'string', virtual: true},
    
    {name: 'name',                   type: 'string'},
    {name: 'is_default?',            type: 'bool', virtual: true},
    
    {name: 'large_filename',         type: 'string', virtual: true},
    {name: 'medium_filename',        type: 'string', virtual: true},
    {name: 'thumb_filename',         type: 'string', virtual: true},
    {name: 'circle_filename',        type: 'string', virtual: true},
    {name: 'hexagon_filename',       type: 'string', virtual: true},
    
    {name: 'full_name',              type: 'string'},
    {name: 'nickname',               type: 'string'},
    
    {name: 'website',                type: 'string'},
    {name: 'email',                  type: 'string'},
    {name: 'phone',                  type: 'string'},
    
    {name: 'job_title',              type: 'string'},
    {name: 'sexuality',              type: 'string'},
    {name: 'location',               type: 'string'},
    {name: 'relationship',           type: 'string'},
    {name: 'gender',                 type: 'string'},
    {name: 'birthday',               type: 'string'},
    {name: 'religion',               type: 'string'},
    {name: 'political_view',         type: 'string'},
    {name: 'heritage',               type: 'string'},
    {name: 'diet',                   type: 'string'},
		
    {name: 'activites',              type: 'string'},
    {name: 'topics',                 type: 'string'},
    {name: 'foods',                  type: 'string'},
    {name: 'drinks',                 type: 'string'},
    {name: 'movies',                 type: 'string'},
    {name: 'tv_shows',               type: 'string'},
    {name: 'music',                  type: 'string'},
    {name: 'books',                  type: 'string'},
    {name: 'role_models',            type: 'string'},
    {name: 'places',                 type: 'string'},
    {name: 'quotes',                 type: 'string'},
    
    {name: 'skype',                  type: 'string'},
    {name: 'linked_in',              type: 'string'},
    {name: 'twitter',                type: 'string'},
    {name: 'im',                     type: 'string'},
    {name: 'blog',                   type: 'string'},
    {name: 'other',                  type: 'string'},
    
    {name: 'bio',                    type: 'string'},
    {name: 'bio_html',               type: 'string'},
		
    {name: 'verified_email',         type: 'boolean'},
    
		{name: 'user', virtual: true},
    
    {name: 'last_notified_event_id', type: 'string', virtual: true},
    {name: 'created_at',             type: 'string', virtual: true},
    {name: 'modified_at',            type: 'string', virtual: true},
    
		{
      name: 'birthday_date',
      virtual: true,
      convert: function(value, record) {
				if (!record.birthday) return "";
        return Kohive.OS.convertToDate(record.birthday, true); // TODO change to j-M-y when chronic can support it);
      }
    },

		{
      name: 'birthday_normal',
      virtual: true,
      convert: function(value, record) {
				if (!record.birthday) return "";
        return Kohive.OS.convertToDate(record.birthday, false); // TODO change to j-M-y when chronic can support it);
      }
    },
		
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        // Check if the current users id is equal to the id cards user id
        var currentUserId = Kohive.OS.getCurrentUser().data.id;
        var idCardUserId = record.user_id;
        
        if (currentUserId == idCardUserId) return true;
        
        return false;
      }
    }
  ],
  
  /**
   * @property clickActions
   * @type Array
   * An array of click actions for the id card
   */
  clickActions: (function() {
    // Loop through all fields except virtual and 
    var array = [], fields = [
			'name',
			'full_name',
			'nickname',
			'website',
			'email',
			'phone',
			'job_title',
			'sexuality',
			'location',
			'relationship',
			'birthday',
			'religion',
			'political_view',
			'heritage',
			'gender',
			'activites',
			'topics',
			'foods',
			'diet',
			'drinks',
			'movies',
			'tv_shows',
			'music',
			'books',
			'role_models',
			'places',
			'quotes',
			'bio',
			'avatar',
			
			//extra added 27/11/09
			'skype',
			'linked_in',
			'twitter',
			'im',
			'blog',
			'other'
		];
		
    var defaults = {
      type:       'edit',
      method:     'dblclick'
    };
    
    // Loop through all the fields (above) and create the clickActions for the model from them
    Ext.each(fields, function(field) {
			if (field == "avatar") return;
			
      var obj = Ext.applyIf({
        name:  field,
        value: field
      }, defaults);
      
      if (field == "bio") { obj.fieldType = 'textarea'; obj.value = "bio"; obj.forceHeight = true; obj.forceRefresh = true; };
      if (field == "name" || field == "full_name" || field == "nickname") { obj.allowBlank = false; };
			
			if (field == "birthday") { obj.value = "birthday_date"; obj.forceRefresh = true; obj.type = "datePicker"; };
			if (field == "gender") { obj.type = "genderPicker"; };

			if (field == "email") { obj.vtype = "email"; };
			if (field == "website") { obj.vtype = "urlWithoutHttp"; };
      
      array.push(obj);
    }, this);
    
    return array;
  })(),

	/**
	 * Creates a new datefield
	 */
	datePicker: function(dataview, record, el, a, index, indexWindow) {
		var sibling = el.insertSibling({
			id:  'edit-birthday-field',
			cls: 'edit-birthday-field',
			children: [
			  {
			    id:  'edit-birthday-day',
			    tag: 'select',
			    children: [
			      {tag:'option', html:'1'},
			      {tag:'option', html:'2'},
			      {tag:'option', html:'3'},
			      {tag:'option', html:'4'},
			      {tag:'option', html:'5'},
			      {tag:'option', html:'6'},
			      {tag:'option', html:'7'},
			      {tag:'option', html:'8'},
			      {tag:'option', html:'9'},
			      {tag:'option', html:'10'},
			      {tag:'option', html:'11'},
			      {tag:'option', html:'12'},
			      {tag:'option', html:'13'},
			      {tag:'option', html:'14'},
			      {tag:'option', html:'15'},
			      {tag:'option', html:'16'},
			      {tag:'option', html:'17'},
			      {tag:'option', html:'18'},
			      {tag:'option', html:'19'},
			      {tag:'option', html:'20'},
			      {tag:'option', html:'21'},
			      {tag:'option', html:'22'},
			      {tag:'option', html:'23'},
			      {tag:'option', html:'24'},
			      {tag:'option', html:'25'},
			      {tag:'option', html:'26'},
			      {tag:'option', html:'27'},
			      {tag:'option', html:'28'},
			      {tag:'option', html:'29'},
			      {tag:'option', html:'30'},
			      {tag:'option', html:'31'}
			    ]
			  },
			  {
			    id:  'edit-birthday-month',
			    tag: 'select',
			    children: [
			      {tag:'option', value:'1', html:'Jan'},
			      {tag:'option', value:'2', html:'Feb'},
			      {tag:'option', value:'3', html:'Mar'},
			      {tag:'option', value:'4', html:'Apr'},
			      {tag:'option', value:'5', html:'May'},
			      {tag:'option', value:'6', html:'Jun'},
			      {tag:'option', value:'7', html:'Jul'},
			      {tag:'option', value:'8', html:'Aug'},
			      {tag:'option', value:'9', html:'Sep'},
			      {tag:'option', value:'10', html:'Oct'},
			      {tag:'option', value:'11', html:'Nov'},
			      {tag:'option', value:'12', html:'Dec'}
			    ]
			  },
			  {
			    id:    'edit-birthday-year',
			    tag:   'input',
			    type:  'text',
			    value: '1981',
			    vtype: 'bdayyear'
			  }
			]
		});
		
		el.enableDisplayMode();
		el.hide();
		
		var scope = this;
		
		var onSubmit = function() {
		  var day   = Ext.get('edit-birthday-day').dom.value,
		      month = Ext.get('edit-birthday-month').dom.value,
		      year  = Ext.get('edit-birthday-year').dom.value;
      
      var date = new Date(year, month - 1, day);
      
      if (date) {
        record.set('birthday', date.format('d M y')); // TODO change to j-M-y when chronic can support it
        record.set('birthday_date', date.format('d-M-y')); // TODO change to j-M-y when chronic can support it
      } else {
        record.set('birthday', '');
        record.set('birthday_date', '');
      };

      record.save({
        url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/id_cards/' + record.get('id'),
        scope:   this,
        success: function() {
          sibling.remove();
          el.show();
        },
        failure: function() {
          
        }
      });
		};
		
		var onClick = function() {
      if (scope.emailTimer) clearTimeout(scope.emailTimer);
      scope.emailTimer = setTimeout(onSubmit, 1000);
		};
		
		Ext.getBody().on('mousedown', function(e) {
		  var el = Ext.get(e.getTarget());
		  
		  if (el.hasClass('edit-birthday-field') || el.parent().hasClass('edit-birthday-field') || el.parent().parent().hasClass('edit-birthday-field') || el.parent().parent().parent().hasClass('edit-birthday-field')) {
		    clearTimeout(scope.emailTimer);
		  } else {
		    onClick();
		  };
		}, this);
		
    // var dateField = new Ext.form.DateField({
    //  renderTo:   'edit-birthday-field',
    //  name:       'birthday',
    //  value:      (record.get('birthday_normal')) ? record.get('birthday_normal') : '',
    //       showToday:  false,
    //  width:      110,
    //  format:     'd-M-y', // TODO change to j-M-y when chronic can support it
    //  listeners: {
    //    scope   : this,
    //    'focus' : function(field) {
    //      field.onTriggerClick();
    //    },
    //    'blur'  : function(field) {
    //      var date = field.getValue();
    //      
    //      if (date) {
    //        record.set('birthday', date.format('d M y')); // TODO change to j-M-y when chronic can support it
    //        record.set('birthday_date', date.format('d-M-y')); // TODO change to j-M-y when chronic can support it
    //      } else {
    //        record.set('birthday', '');
    //        record.set('birthday_date', '');
    //      };
    //      
    //      record.save({
    //        url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/id_cards/' + record.get('id'),
    //        scope:   this,
    //        success: function() {
    //          sibling.remove();
    //          el.show();
    //        },
    //        failure: function() {
    //          
    //        }
    //      });
    //    }
    //  }
    // });
	},
	
	/**
	 * Creates a new combobox for gender
	 */
	genderPicker: function(dataview, record, el, a, index, indexWindow) {
		var sibling = el.insertSibling({
			id:  'edit-gender-field',
			cls: 'edit-birthday-field'
		});
		
		el.enableDisplayMode();
		el.hide();
		
		var dateField = new Ext.form.ComboBox({
			renderTo:   'edit-gender-field',
			name:       'gender',
			value:      record.get('gender'),
			width:      110,
			store:      new Ext.data.SimpleStore({
          fields: ['gender', 'label'],
          data : [
            ['male',   Ext.ux.Translator.translate('male')],
            ['female', Ext.ux.Translator.translate('female')]
          ]
      }),
			selectOnFocus: true,
      triggerAction: 'all',
      displayField:  'label',
      typeAhead:     true,
      emptyText:     '',
      mode:          'local',
			listeners: {
				scope   : this,
				'blur'  : function(field) {
					var gender = field.getValue();
					if (!gender) {
						sibling.remove();
						el.show();
						
						return;
					};
					
					record.set('gender', gender);
					record.save({
						url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/id_cards/' + record.get('id'),
						scope:   this,
						success: function() {
							sibling.remove();
							el.show();
						},
						failure: function() {
							
						}
					});
				}
			}
		});
		
		var tempTask = new Ext.util.DelayedTask(function() {
		  dateField.focus();
		}, this);
		tempTask.delay(100);
	}
});

/**
 * @class Kohive.models.Image
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Image", {
  fields:    [
    {name: 'id',                   type: 'int', virtual: true},
    {name: 'album_id',             type: 'int'},
    {name: 'author_membership_id', type: 'int'},
    {name: 'size',                 type: 'string', virtual: true},
    {name: 'content_type',         type: 'string', virtual: true},
    {name: 'file_url',             type: 'string'},
    {name: 'filename',             type: 'string', virtual: true},
    {name: 'height',               type: 'int', virtual: true},
    {name: 'width',                type: 'int', virtual: true},
    {name: 'parent_id',            type: 'int', virtual: true},
    {name: 'thumbnail',            type: 'string', virtual: true},
    {name: 'title',                type: 'string'},
    {name: 'description',          type: 'string'},
    {name: 'description_html',     type: 'string'},
    {name: 'taken_at',             type: 'string', virtual: true},
    {name: 'created_at',           type: 'string'},
    {name: 'updated_at',           type: 'string'},
    {name: 'thumb_filename',       type: 'string', virtual: true},
    {name: 'medium_filename',      type: 'string', virtual: true},
    {name: 'original_filename',    type: 'string', virtual: true},
    {name: 'is_remote_image',      type: 'bool', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
   	{
			name: 'tag_list',
			convert: function(value, record) {
				if (!value && record.tags.length > 0) {
					var res = "";
					Ext.each(record.tags, function(tag) {
					  res += tag.name + " ";
					}, this);
					return res;
				} else {
					return value;
				}
			}
		},
    {
      name: 'tags',
      virtual: true,
      convert: function(value, record) {
        if (value.length < 1) value = false;
        
        return value;
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
    {
      name: 'id_card',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.getIdCardFromMembershipId(record.author_membership_id);
      }
    },
    
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  belongsTo: "Album",
  hasMany:   "Comment",
  
  /**
	 * 
	 */
	editTags: function(dataview, record, el, a, index, indexWindow) {
	  var sibling = el.insertSibling({
			id:  'edit-tags-field'
		});
		
		el.enableDisplayMode();
		el.hide();
	  
	  this.tagsField = new Ext.ux.form.SuperBoxSelect({
	    renderTo:        'edit-tags-field',
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      store:           indexWindow.tagsStore,
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        newitem: function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        },
        expand: function() {
          var list = this.tagsField.list;
          
          var tempTask = new Ext.util.DelayedTask(function() {
            list.setTop(list.getTop() + 3);
          }, this);
          tempTask.delay(200);
        },
        'enter-key': function(field) {
          this.saveTags(field, record, sibling, el);
        },
        blur: function(field) {
          this.saveTags(field, record, sibling, el);
				}
      }
		});
		
		this.tagsField.setWidth(sibling.getWidth());
    
    //format tags
    var tags = Ext.util.Format.trim(record.get('tag_list')).split(' ');
		
		var array = [];
		Ext.each(tags, function(t) {
		  array.push({
		    tag: t
		  });
		}, this);
		
		//set the tags value
    this.tagsField.setValueEx(array);
    
    this.tagsField.focus();
	},
	
	saveTags: function(field, record, sibling, el, dataview) {
		var val = Ext.util.Format.trim(this.tagsField.getValue()).split(',');
		
		if (val.length > 0) {
		  if (val.length == 1 && val[0] == "") {
		    record.set('tag_list', '');
  			record.set('tags', false);
  			this.tagsField.setValue('');
		  } else {
  		  var newArray = [];
		  
  		  Ext.each(val, function(a) {
  		    newArray.push({name:a});
  		  }, this);
		  
  			record.set('tag_list', val.join(' '));
  			record.set('tags', false);
  			record.set('tags', newArray);
  			this.tagsField.setValue(val);
  		};
		};
		
		this.showUpdating();
		
		record.save({
		  url:   Kohive.OS.scopedUrl(String.format("albums/{0}/images/{1}", record.get('album_id'), record.get('id'))),
			scope: this,
			success: function() {
				sibling.remove();
				el.show();
				this.hideLoading();
			},
			failure: Ext.emptyFn
		});
	},
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('photos');
		var view = controller.indexWindow || null;
		
		if (view) view.showUpdating();
	},
	
	/**
	 * Hides the loading animation from the view
	 */
	hideLoading: function() {
	  var controller = Kohive.OS.getController('photos');
		var view = controller.indexWindow || null;
		
		if (view) {
		  view.hideLoading();
		  view.fireEvent('update-filter-bar', view.cachedStore);
		};
	},
	
	/**
	 * Refreshes the content in the view
	 */
	refreshView: function() {
		var controller = Kohive.OS.getController('photos');
		var view = controller.indexWindow || null;
		
    if (view && view.store) view.store.load();
		if (view) view.hideLoading();
	}
});

/**
 * @class Kohive.models.MicroBlog
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.MicroBlog", {
  modelName: 'microblog',
  fields:    [
    {name: 'twitter_id',           type: 'int', virtual: true},
    {name: 'id',                   type: 'int', virtual: true},
    {name: 'hive_id',              type: 'int', virtual: true},
    {name: 'author_membership_id', type: 'int'},
    {name: 'nickname',             type: 'string', virtual: true},
    {name: 'created_at',           type: 'string', virtual: true},
    {name: 'modified_at',          type: 'string', virtual: true},
    
    {name: 'author_avatar',        type: 'string', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {name: 'toggled',          type: 'boolean', virtual: true},
    {name: 'toggled_comments', type: 'boolean', virtual: true},
    
    {
      name   : 'comments',
      virtual: true,
      convert: function(value, record) {
        if (value) return value;
        else [];
      }
    },
    {name: 'new_comment',   type: 'int',     virtual: true},
    {name: 'hide_comments', type: 'boolean', virtual: true},
    
		{
			name: 'body',
			convert: function(value, record) {
				var url = "<br />";
				
				//get the twitpic links
        var urls = value.match(new RegExp("(http:\/\/[www\.]*[a-zA-Z0-9\-\_]*\.[a-z]{2,3}[\.a-z]{3,4}[\-\/\?\=\+\.\_a-zA-Z0-9]*)", 'g')) || [];
        Ext.each(urls, function(link) {
          var twitpic = (link.split('twitpic.com/')) ? link.split('twitpic.com/')[1] : null;
          var yfrog   = (link.split('yfrog.com/')) ? link.split('yfrog.com/')[1] : null;
          
         // add the url
         if (twitpic) url += '<a href="' + link + '" align="left"><img class="img" src="http://twitpic.com/show/mini/' + twitpic + '" /></a>&nbsp;';
         if (yfrog) url += '<a href="' + link + '" align="left"><img class="img" src="http://yfrog.com/' + yfrog + '.th.jpg" /></a>&nbsp;';
        }, this);
				
				//add the urls
        if (urls.length > 0) value += url;
				
				return value;
			}
		},
		
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
    
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
        if (Kohive.OS.isAuthor(record) || Kohive.OS.isCustom(record, 'assignee_membership_id', 'id') || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//mod/owner
				// if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  dataView: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
       '<tpl for=".">',
         '<div id="microblog-{id}" class="x-pod x-microblog">',
           '<div class="x-content-wrap">',
             '<div class="x-control-wrap"><div class="x-control"><div></div></div></div>',
             '<div class="x-content">',
               '<div class="x-title"><div class="x-title">{author_nickname}</div></div>',
               // '<div class="x-title"><div class="x-title">{nickname}</div><div class="x-reply-btn action-click-reply"></div></div>',
               '<tpl if="!toggled">',
                 '<div class="x-extra">',
                   '<div class="x-message">',
                      '<div class="x-avatar">',
                        '<img src="{author_avatar}" />',
                      '</div>',
                      '<div class="action-edit-body">{body}</div>',
                      '<div class="x-clearfix"></div>',
                    '</div>',
                   '<div class="x-other">',
                     '<div class="x-created-on">' + '{created_at_date}</div>',
                   '</div>',
                 '</div>',
               '</tpl>',
             '</div>',
             '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
           '</div>',
           
           '<tpl if="!toggled">',
             '<div class="x-comments<tpl if="this.isSmall(comments, toggled_comments, new_comment)"> x-small</tpl><tpl if="this.hasComments(comments)"> x-hascomments</tpl><tpl if="new_comment"> x-adding-comment</tpl>">',
               '<tpl if="!toggled_comments">',
                 '<tpl for="comments">',
                   '<div class="x-comment" id="microblog-comment-{#}-{id}">',
                     '<div class="x-delete-comment"><div></div></div>',
                     '<div class="x-author"><img src="{thumb_filename}" /> {[Kohive.OS.isAuthor(values) ? Ext.ux.Translator.translate("me") : values.author_nickname]}</div>',
                     '<div class="x-body">{content}</div>',
                     '<div class="x-date">{[(values.created_at.split("T")[1]) ? Kohive.OS.convertToDate(values.created_at, true) : values.created_at]}</div>',
                   '</div>',
                 '</tpl>',
               '</tpl>',
               '<tpl if="!this.isSmall(comments, toggled_comments, new_comment)">',
                 '<div class="x-new-comment<tpl if="this.hasComments(comments)"> x-extended</tpl><tpl if="new_comment"> x-is-field</tpl>">',
                   '<tpl if="!new_comment && this.hasComments(comments)"><div class="x-toggle-button"><div class="x-img"></div></div></tpl>',
                   '<tpl if="!new_comment"><div class="x-button"><div class="x-img"></div></div></tpl>',
                   '<tpl if="new_comment">',
                     '<div class="x-field"><input type="text" /></div>',
                     '<div class="x-submit"><span>' + Ext.ux.Translator.translate('post') + '</span></div>',
                   '</tpl>',
                 '</div>',
               '</tpl>',
               '<tpl if="this.isSmall(comments, toggled_comments, new_comment)">',
                 '<div class="x-new-comment x-small">',
                   '<tpl if="this.hasComments(comments)"><div class="x-toggle-button toggled"><div class="x-img"></div><span class="x-txt">{[this.commentsLength(values.comments)]}</span></div></tpl>',
                   '<tpl if="!this.hasComments(comments)"><div class="x-button"><div class="x-img"></div><span class="x-txt">{[Ext.ux.Translator.translate("new_comment")]}</span></div></tpl>',
                 '</div>',
               '</tpl>',
             '</div>',
           '</tpl>',
           
           {
             isSmall: function(comments, toggled_comments, new_comment) {
               if (new_comment) return false;
               if (comments.length == 0 && toggled_comments) return true;
               if (comments.length == 0 && !toggled_comments) return true;
               if (comments.length > 0 && toggled_comments) return true;
               return false;
             },
             hasComments: function(comments) {
               return comments.length > 0;
             },
             commentsLength: function(comments) {
               var length = comments.length;
               if (comments.length == 0) return length + " " + Ext.ux.Translator.translate('comments');
               if (comments.length == 1) return length + " " + Ext.ux.Translator.translate('comment');
               if (comments.length > 1) return length + " " + Ext.ux.Translator.translate('comments');
             }
           },
           
         '</div>',
       '</tpl>',
     '</div>'
  ),
  
  clickActions: [
    // {
    //   name:      'body',
    //   type:      'edit',
    //   fieldType: 'textarea',
    //   method:    'dblclick'
    // },
    {
      name:      'reply',
      type:      'reply',
      method:    'click'
    }
  ]
});

/**
 * @class Kohive.models.Overview
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Overview", {
	modelName: 'overview',
  fields:    [
    {name: 'other',           type: 'string'},
    {name: 'other2',          type: 'string'},
    {name: 'extra_old',       type: 'string'},
    {name: 'extra_new',       type: 'string'},
    {name: 'reference_type',  type: 'string', virtual: true},
    {name: 'author_id',       type: 'int',    virtual: true},
    {name: 'author_avatar',   type: 'string', virtual: true},
    {name: 'event_type',      type: 'string', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_id) return value;
        if (id == record.author_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
		{
			name: 'icon',
			virtual: true,
			convert: function(value, record) {
				var name = record.reference_type.toLowerCase(),
				    path = "";
				
				if (name == "hive") path = "/images/apps/Overview/" + name + ".png";
				else if (name == "membership") path = record.author_avatar;
				else if (name == "image" || name == "comment" || name == "album") path = "/images/components/ApplicationLauncher/icons/photos.png";
				else if (name == "blog") path = "/images/components/ApplicationLauncher/icons/blog.png";
				else if (name == "filecabinet") path = "/images/components/ApplicationLauncher/icons/filecabinet.png";
				else if (name == "videoalbum") path = "/images/components/ApplicationLauncher/icons/videos.png";
				else path = "/images/components/ApplicationLauncher/icons/" + name.pluralize() + ".png";
				
				return path;
			}
		},
		{
			name: 'translation',
			virtual: true,
			convert: function(value, record) {
				var tr = (record.reference_type == "Task" && record.event_type == "update_status") ? 'event_' + record.reference_type.toLowerCase() + '_' + record.event_type.toLowerCase() + '_' + record.extra_new : 'event_' + record.reference_type.toLowerCase() + '_' + record.event_type.toLowerCase();
				if (tr == "event_image_destroy") console.log();
				
				return Ext.ux.Translator.translate(tr, {
					title:           record.other,
					author_nickname: record.author_nickname,
					other:           record.other2,
					extra_old:       record.extra_old,
					extra_new:       record.extra_new,
					created_at:      record.created_at_date
				});
			}
		}
    // {
    //   name: 'id_card',
    //   virtual: true,
    //   convert: function(value, record) {
    //     return Kohive.OS.getIdCardFromMembershipId(record.author_membership_id);
    //   }
    // }
  ]
});

/**
 * @class Kohive.models.Sticky
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Sticky", {
  modelName: 'sticky',
  fields:    [
    {name: 'id',                   type: 'int'   },
    {name: 'body',                 type: 'string'},
    {name: 'body_html',            type: 'string'},
    {name: 'color',                type: 'string'},
    {name: 'stamp',                type: 'string'},
    {name: 'author_membership_id', type: 'int'},
    {name: 'created_at',           type: 'string'},
    {name: 'modified_at',          type: 'string'},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {name: 'toggled', type: 'boolean', virtual: true},
    
    {
      name: 'color_cls',
      virtual: true,
      convert: function(value, record) {
        return record.color + (Math.floor(Math.random() * 3) + 1);
      }
    },
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
   	
		//editable properties
		{
      name: 'can_drag',
      virtual: true,
      convert: function(value, record) {
				//is not used already
				if (Kohive.OS.stickyDropAllowed({data:record})) return true;
				return false;
      }
    },
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  dataView: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
      '<tpl for=".">',
        '<div id="sticky-{id}" class="x-sticky {color} {color_cls} x-pod<tpl if="can_delete"> x-deletable-hover</tpl><tpl if="can_drag"> x-dragable</tpl>">',
      	  '<div class="x-stamp<tpl if="stamp"> {stamp}</tpl>">',
      	    '<div class="x-created">{author_nickname}, {created_at_date}</div>',
            '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
      	  '</div>',
      	  '<div id="middle" class="x-sticky-wrap">',
        	  '<div id="inner" class="x-sticky-inner-wrap">',
              '<div class="x-body action-edit-body">{body_html}</div>',
            '</div>',
      	  '</div>',
      	'</div>',
      '</tpl>',
    '</div>'
  ),
  
  clickActions: [
    {
      name:      'body',
      value:     'body',
      type:      'edit',
      fieldType: 'textarea',
      method:    'dblclick',
			forceRefresh: true
    }
  ]
});

    

/**
 * @class Kohive.models.Task
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Task", {
  modelName: 'task',
  fields:    [
    {name: 'id',                     type: 'int', virtual: true},
    {name: 'title',                  type: 'string'},
    {name: 'body',                   type: 'string'},
    {name: 'body_html',              type: 'string', virtual: true},
    {name: 'due',                    type: 'string'},
    {name: 'author_membership_id',   type: 'int'},
    {name: 'assignee_membership_id', type: 'int'},
    {name: 'created_at',             type: 'string', virtual: true},
    {name: 'modified_at',            type: 'string', virtual: true},
    {name: 'completed_at',           type: 'string', virtual: true},

    {name: 'milestone_id', type: 'int'},
    {name: 'milestone',    virtual: true},
    
    {name: 'file_cabinet_id', type: 'int'},
    {name: 'file_cabinet',    virtual: true},
    
    {name: 'toggled',          type: 'boolean', virtual: true},
    {name: 'toggled_comments', type: 'boolean', virtual: true},
    
    {name: 'width', virtual: true},
    
    {
      name   : 'comments',
      virtual: true,
      convert: function(value, record) {
        if (value) return value;
        else [];
      }
    },
    {name: 'new_comment',   type: 'int',     virtual: true},
    {name: 'hide_comments', type: 'boolean', virtual: true},
    
    {
      name:    'title_trimmed',
      virtual: true,
      convert: function(value, record) {
        if (!value) return record.title;
        return value;
      }
    },
    
    {name: 'type', type: 'int'},
    {name: 'task_type', type: 'int'},
    {
      name:    'type_string',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var value = (record.type == null) ? 0 : record.type,
            valueString = 0;
        
        switch (value) {
          case 0:
            valueString = "null";
            break;
          
          case 1:
            valueString = "cube";
            break;
          
          case 2:
            valueString = "star";
            break;
          
          case 3:
            valueString = "spider";
            break;
          
          case 4:
            valueString = "bug";
            break;
            
          case 5:
            valueString = "brush";
            break;
          
          case 6:
            valueString = "console";
            break;
          
          case 7:
            valueString = "tool";
            break;
          
          case 8:
            valueString = "colors";
            break;
        };
        
        return valueString;
      }
    },
    
    {name: 'flagged', type: 'boolean'},
    {
      name:    'flagged_string',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var value       = (record.flagged == null) ? false : record.flagged,
            valueString = "";
        
        switch (value) {
          case false:
            valueString = "false";
            break;
          
          case true:
            valueString = "true";
            break;
        };
        
        return valueString;
      }
    },
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    {
      name:    'assignee_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.assignee_membership_id) return value;
        if (id == record.assignee_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
    {
      name: 'state',
      convert: function(value, record) {
        // TODO check if it is overdue
        return value;
      }
    },
    
    {
      name: 'priority',
      convert: function(value, record) {
        return (value == null) ? 1 : value;
      }
    },
    {
      name: 'priority_string',
      virtual: true,
      convert: function(value, record) {
        value = (record.priority == null) ? 1 : record.priority;
        var valueString;
        
        switch (value) {
          case 1:
            valueString = "low";
            break;
          
          case 2:
            valueString = "medium";
            break;
          
          case 3:
            valueString = "high";
            break;
        };
        
        return valueString;
      }
    },
    
    {
      name: 'effort',
      convert: function(value, record) {
        return (value == null) ? 1 : value;
      }
    },
    {
      name: 'effort_string',
      virtual: true,
      convert: function(value, record) {
        value = (record.effort == null) ? 1 : record.effort;
        var valueString;
        
        switch (value) {
          case 1:
            valueString = "low";
            break;
          
          case 2:
            valueString = "medium";
            break;
          
          case 3:
            valueString = "high";
            break;
        };
        
        return valueString;
      }
    },
    
		{
			name: 'overdue',
			virtual: true,
			convert: function(value, record) {
				// Check if a due date is set, as if none is set it cant be overdue
				if (!record.due) return;
				
				// Convert the due date into a proper JS date, and get the current date
				var dueDate = Kohive.OS.convertToDate(record.due);
				var currentDate = new Date();
				
				// Check if the due date is below or lower than the current date
				if (dueDate < currentDate && record.state == "new") return true;
				
				return false;
			}
 		},
		
    {
			name: 'tag_list',
			convert: function(value, record) {
				if (!value && record.tags.length > 0) {
					var res = "";
					Ext.each(record.tags, function(tag) {
					  res += tag.name + " ";
					}, this);
					return res;
				} else {
					return value;
				}
			}
		},
    {
      name: 'tags',
      virtual: true,
      convert: function(value, record) {
        if (value.length < 1) value = false;
        
        return value;
      }
    },
    
    {
      name: 'completed_at_date',
      virtual: true,
      convert: function(value, record) {
        if (!record.completed_at) return false;
        return Kohive.OS.convertToDate(record.completed_at, true);
      }
    },
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
		{
      name: 'due_date',
      virtual: true,
      convert: function(value, record) {
				if (record.due) return Kohive.OS.convertToDate(record.due, true);
				else return false;
      }
    },
		{
      name: 'due_normal',
      virtual: true,
      convert: function(value, record) {
				if (record.due) return Kohive.OS.convertToDate(record.due, false);
				else return false;
      }
    },
		
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isCustom(record, 'assignee_membership_id', 'id') || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isCustom(record, 'assignee_membership_id', 'id') || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  hasMany: "Comment",
  
  dataView: new Ext.XTemplate(
     '<div class="x-pod-wrap">',
        '<tpl for=".">',
        
          '<div id="task-{id}" class="x-pod x-task {state}<tpl if="overdue"> overdue</tpl><tpl if="can_edit"> x-editable-hover</tpl><tpl if="can_delete"> x-deletable-hover</tpl>">',
            '<div class="x-content-wrap">',
            
              '<div class="x-control-wrap"><div class="x-control<tpl if="toggled"> hidden</tpl>"><div></div></div></div>',
              
              '<div class="x-content">',
                '<div class="x-title">',
                  '<div class="x-state action-updateState-state {state}"></div>',
                  '<div class="x-title action-edit-title {state}">{[(values.title_trimmed) ? values.title_trimmed : values.title]}</div>',
                  
                  '<tpl if="toggled">',
                    '<div class="x-toggle-extra">',
                      '<tpl if="tags"><span class="x-tag"></span></tpl>',
                      '<tpl if="body"><span class="x-note"></span></tpl>',
                      '<span class="x-type action-updateType-type {type_string}"></span>',
                      '<span class="x-priority action-updatePriority-priority {priority_string}"></span>',
                      '<span class="x-effort action-updateEffort-effort {effort_string}"></span>',
                      '<span class="x-flagged action-updateFlagged-flagged {flagged_string}"></span>',
                    '</div>',
                  '</tpl>',
                  '<tpl if="!toggled">',
                    '<div class="x-toggle-extra">',
                      '<span class="x-type action-updateType-type {type_string}"></span>',
                      '<span class="x-priority action-updatePriority-priority {priority_string}"></span>',
                      '<span class="x-effort action-updateEffort-effort {effort_string}"></span>',
                      '<span class="x-flagged action-updateFlagged-flagged {flagged_string}"></span>',
                    '</div>',
                  '</tpl>',
                '</div>',
                
                '<tpl if="!toggled">',
                  '<div class="x-extra">',
  									'<tpl if="assignee_nickname || can_edit">',
  									'<div class="x-stuff">',
  										'<div class="x-assignee action-assignedPicker-assignee_nickname {state}<tpl if="!assignee_nickname && can_edit"> empty</tpl>">',
  											'<tpl if="assignee_nickname">{assignee_nickname}</tpl><tpl if="!assignee_nickname && can_edit">' + Ext.ux.Translator.translate('app_tasks_add_assignee') + '</tpl>',
  										'</div>',
  									'</div>',
  									'</tpl>',
  									'<tpl if="due_date || can_edit">',
  									'<div class="x-stuff date">',
  										'<tpl if="due_date"><div class="x-due action-duePicker-due">{due_date}</div><div class="x-due-icon"></div></tpl>',
  	                  '<tpl if="!due_date && can_edit"><div class="x-due x-empty action-duePicker-due">' + Ext.ux.Translator.translate('app_tasks_add_due_date') + '</div><div class="x-due-icon"></div></tpl>',
  									'</div>',
  									'</tpl>',
  									'<tpl if="milestone || can_edit">',
  									'<div class="x-stuff milestone">',
  										'<tpl if="milestone"><div class="x-milestone action-milestonePicker-milestone"><tpl for="milestone">{title}</tpl> </div><div class="x-milestone-icon"></div></tpl>',
  	                  '<tpl if="!milestone && can_edit"><div class="x-milestone x-empty action-milestonePicker-milestone">' + Ext.ux.Translator.translate('app_tasks_add_milestone') + '</div><div class="x-milestone-icon"></div></tpl>',
  									'</div>',
  									'</tpl>',
                    '<tpl if="body">',
                      '<div class="x-description"><div class="action-edit-body">{body_html}</div></div>',
                    '</tpl>',
                    '<tpl if="!body && can_edit">',
                      '<div class="x-description x-empty"><div class="action-edit-body">' + Ext.ux.Translator.translate('app_add_description') + '</div></div>',
                    '</tpl>',
                    '<tpl if="file_cabinet || can_edit">',
  									'<div class="x-stuff file">',
  										'<div class="x-file action-filesPicker-file_id<tpl if="!file_cabinet && can_edit"> empty</tpl>">',
  											'<tpl if="file_cabinet"><tpl for="file_cabinet">{title}</tpl></tpl><tpl if="!file_cabinet && can_edit">' + Ext.ux.Translator.translate('app_tasks_file') + '</tpl>',
  										'</div>',
  									'</div>',
  									'</tpl>',
  									'<tpl if="tags">',
                      '<div class="x-tags">',
                        '<div class="action-editTags-tags">',
                          '<div class="x-form-field-wrap x-trigger-field-wrap">',
                            '<div class="x-form-text x-superboxselect  x-superboxselect-display-btns">',
                              '<ul>',
                                '<tpl for="tags">',
                                  '<li class="x-superboxselect-item no-close">{name}</li>',
                                '</tpl>',
                              '</ul>',
                            '</div>',
                          '</div>',
                        '</div>',
                      '</div>',
                    '</tpl>',
                    '<tpl if="!tags && can_edit">',
                      '<div class="x-tags x-empty"><div class="action-editTags-tags">' + Ext.ux.Translator.translate('app_add_tags') + '</div></div>',
                    '</tpl>',
                    '<div class="x-other">',
                      '<div class="x-created-on">' + '{author_nickname}, {created_at_date}</div>',
                      '<tpl if="completed_at_date"><div class="x-created-by">' + Ext.ux.Translator.translate('completed_on') + ' {completed_at_date}</div></tpl>',
                    '</div>',
                  '</div>',
                '</tpl>',
                
              '</div>',
              '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
            '</div>',
            
            '<tpl if="!toggled">',
              '<div class="x-comments<tpl if="this.isSmall(comments, toggled_comments, new_comment)"> x-small</tpl><tpl if="this.hasComments(comments)"> x-hascomments</tpl><tpl if="new_comment"> x-adding-comment</tpl>">',
                '<tpl if="!toggled_comments">',
                  '<tpl for="comments">',
                    '<div class="x-comment" id="task-comment-{#}-{id}">',
                      '<div class="x-delete-comment"><div></div></div>',
                      '<div class="x-author"><img src="{thumb_filename}" /> {[Kohive.OS.isAuthor(values) ? Ext.ux.Translator.translate("me") : values.author_nickname]}</div>',
                      '<div class="x-body">{content}</div>',
                      '<div class="x-date">{[(values.created_at.split("T")[1]) ? Kohive.OS.convertToDate(values.created_at, true) : values.created_at]}</div>',
                    '</div>',
                  '</tpl>',
                '</tpl>',
                '<tpl if="!this.isSmall(comments, toggled_comments, new_comment)">',
                  '<div class="x-new-comment<tpl if="this.hasComments(comments)"> x-extended</tpl><tpl if="new_comment"> x-is-field</tpl>">',
                    '<tpl if="!new_comment && this.hasComments(comments)"><div class="x-toggle-button"><div class="x-img"></div></div></tpl>',
                    '<tpl if="!new_comment"><div class="x-button"><div class="x-img"></div></div></tpl>',
                    '<tpl if="new_comment">',
                      '<div class="x-field"><input type="text" /></div>',
                      '<div class="x-submit"><span>' + Ext.ux.Translator.translate('post') + '</span></div>',
                    '</tpl>',
                  '</div>',
                '</tpl>',
                '<tpl if="this.isSmall(comments, toggled_comments, new_comment)">',
                  '<div class="x-new-comment x-small">',
                    '<tpl if="this.hasComments(comments)"><div class="x-toggle-button toggled"><div class="x-img"></div><span class="x-txt">{[this.commentsLength(values.comments)]}</span></div></tpl>',
                    '<tpl if="!this.hasComments(comments)"><div class="x-button"><div class="x-img"></div><span class="x-txt">{[Ext.ux.Translator.translate("new_comment")]}</span></div></tpl>',
                  '</div>',
                '</tpl>',
              '</div>',
            '</tpl>',
            
            {
              isSmall: function(comments, toggled_comments, new_comment) {
                if (new_comment) return false;
                if (comments.length == 0 && toggled_comments) return true;
                if (comments.length == 0 && !toggled_comments) return true;
                if (comments.length > 0 && toggled_comments) return true;
                return false;
              },
              hasComments: function(comments) {
                return comments.length > 0;
              },
              commentsLength: function(comments) {
                var length = comments.length;
                if (comments.length == 0) return length + " " + Ext.ux.Translator.translate('comments');
                if (comments.length == 1) return length + " " + Ext.ux.Translator.translate('comment');
                if (comments.length > 1) return length + " " + Ext.ux.Translator.translate('comments');
              }
            },
            
          '</div>',
        '</tpl>',
      '</div>'
  ),
  
  clickActions: [
    {
      name:       'title',
      value:      'title',
      type:       'edit',
      method:     'dblclick',
      allowBlank: false
    },
    {
      name:       'assignee_nickname',
      method:     'dblclick',
			type:       'assignedPicker'
    },
		{
			name:         'file_id',
			method:       'dblclick',
      forceRefresh: true,
			type:         'filesPicker'
		},
		{
      name:         'due',
      method:       'dblclick',
			type:         'duePicker',
      forceRefresh: true
    },
  	{
      name:         'milestone',
      method:       'dblclick',
			type:         'milestonePicker',
      forceRefresh: true
    },
    {
      name:         'body',
			value:        'body',
      type:         'edit',
      fieldType:    'textarea',
			forceRefresh: true,
      method:       'dblclick',
      emptyText:    Ext.ux.Translator.translate('app_tasks_add_description')
    },
    {
      name:   'tags',
      type:   'editTags',
      method: 'dblclick'
    },
    {
      name:   'state',
      type:   'updateState',
      method: 'click'
    },
    {
      name:   'priority',
      type:   'updatePriority',
      method: 'click'
    },
    {
      name:   'effort',
      type:   'updateEffort',
      method: 'click'
    },
    {
      name:   'type',
      type:   'updateType',
      method: 'click'
    },
    {
      name:   'flagged',
      type:   'updateFlagged',
      method: 'click'
    }
  ],
  
  timer: {},
  
  /**
   * Updates the state of the selected task
   */
  updateState: function(dataview, record, el, a, index) {
    if (!record.get('can_edit')) return;
    
    var newState, state = record.get('state');
    
    // Get the new state
    switch (state) {
      case 'new':
        newState = 'pending';
        break;
        
      case 'pending':
        newState = 'completed';
        break;
      
      case 'completed':
        newState = 'invalid';
        break;
        
      case 'invalid':
        newState = 'new';
        break;
    };
		
		//update the state
		record.set('state', newState);
		
		//update the comepleted_at date if the state has changed
		if (newState == "completed") {
		  record.set('completed_at_date', new Date().format(Kohive.OS.getDefaultDateFormat()));
		} else {
		  record.set('completed_at_date', null);
		};
		
		var scope = this;
		
    clearTimeout(this.timer['state']);
    this.timer['state'] = setTimeout(function() {
      scope.showUpdating();
      
      // Update the record with the new state value
      record.update({
        state: newState
      },{
        url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
        success: scope.hideLoading,
        failure: function() {
          record.set('state', state);
          return Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
        }
      });
    }, 2000);
  },
  
  /**
   * Updates the priority of the selected task
   */
  updatePriority: function(dataview, record, el, a, index) {
    if (!record.get('can_edit')) return;
    
    var newPriority, newPriorityString, priority = record.get('priority'), priorityString = record.get('priority_string');
    
    // Get the new state
    switch (priority) {
      case 1:
        newPriority = 2;
        break;
        
      case 2:
        newPriority = 3;
        break;
      
      case 3:
        newPriority = 1;
        break;
    };
    
    switch (newPriority) {
      case 1:
        newPriorityString = "low";
        break;
      
      case 2:
        newPriorityString = "medium";
        break;
      
      case 3:
        newPriorityString = "high";
        break;
    };
		
    //update the priority
		record.set('priority', newPriority);
		record.set('priority_string', newPriorityString);
		
		var scope = this;
		
    clearTimeout(this.timer['priority']);
    this.timer['priority'] = setTimeout(function() {
      scope.showUpdating();
      
      // Update the record with the new state value
      record.update({
        priority: newPriority,
        priority_string: newPriorityString
      },{
        url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
        success: scope.hideLoading,
        failure: function() {
          record.set('priority', priority);
          record.set('priority_string', priorityString);
          return Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
        }
      });
    }, 2000);
  },
  
  /**
   * Updates the effort of the selected task
   */
  updateEffort: function(dataview, record, el, a, index) {
    if (!record.get('can_edit')) return;
    
    var newEffort, newEffortString, effort = record.get('effort'), effortString = record.get('effort_string');
    
    // Get the new state
    switch (effort) {
      case 1:
        newEffort = 2;
        break;
        
      case 2:
        newEffort = 3;
        break;
      
      case 3:
        newEffort = 1;
        break;
    };
    
    switch (newEffort) {
      case 1:
        newEffortString = "low";
        break;
      
      case 2:
        newEffortString = "medium";
        break;
      
      case 3:
        newEffortString = "high";
        break;
    };
		
		//update the effort
    record.set('effort', newEffort);
    record.set('effort_string', newEffortString);
		
		var scope = this;
		
    clearTimeout(this.timer['effort']);
    this.timer['effort'] = setTimeout(function() {
  		scope.showUpdating();
    
      // Update the record with the new state value
      record.update({
        effort: newEffort,
        effort_string: newEffortString
      },{
        url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
        success: scope.hideLoading,
        failure: function() {
          record.set('effort', effort);
          record.set('effort_string', effortString);
          return Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
        }
      });
    }, 2000);
  },
	
	/**
   * Updates the type of the selected task
   */
  updateType: function(dataview, record, el, a, index) {
    if (!record.get('can_edit')) return;
    
    var newType,
        newTypeString,
        type = (record.get('type')) ? record.get('type') : 0,
        typeString = record.get('type_string');
    
    // Get the new state
    switch (type) {
      case 0:
        newType = 1;
        break;
      
      case 1:
        newType = 2;
        break;
      
      case 2:
        newType = 3;
        break;
      
      case 3:
        newType = 4;
        break;
      
      case 4:
        newType = 5;
        break;
      
      case 5:
        newType = 6;
        break;
      
      case 6:
        newType = 7;
        break;
      
      case 7:
        newType = 8;
        break;
      
      case 8:
        newType = 0;
        break;
    };
    
    switch (newType) {
      case 0:
        newTypeString = "null";
        break;
      
      case 1:
        newTypeString = "cube";
        break;
      
      case 2:
        newTypeString = "star";
        break;
      
      case 3:
        newTypeString = "spider";
        break;
      
      case 4:
        newTypeString = "bug";
        break;
        
      case 5:
        newTypeString = "brush";
        break;
      
      case 6:
        newTypeString = "console";
        break;
      
      case 7:
        newTypeString = "tool";
        break;
      
      case 8:
        newTypeString = "colors";
        break;
    };
		
		//update the priority
		record.set('type', newType);
		record.set('type_string', newTypeString);
		
		var scope = this;
		
    clearTimeout(this.timer['type']);
    this.timer['type'] = setTimeout(function() {
  		scope.showUpdating();
    
      // Update the record with the new type value
      record.update({
        type:        newType,
        type_string: newTypeString
      },{
        url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
        success: scope.hideLoading,
        failure: function() {
          record.set('type', type);
          record.set('type_string', typeString);
          return Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
        }
      });
    }, 2000);
  },
  
  /**
   * Updates the flagged state of the selected task
   */
  updateFlagged: function(dataview, record, el, a, index) {
    if (!record.get('can_edit')) return;
    
    var newFlagged,
        newFlaggedString,
        flagged       = (record.get('flagged')) ? 1 : 0,
        flaggedString = record.get('flagged_string');
    
    // Get the new state
    switch (flagged) {
      case 0:
        newFlagged = 1;
        break;
      
      case 1:
        newFlagged = 0;
        break;
    };
    
    switch (newFlagged) {
      case 0:
        newFlaggedString = "false";
        break;
      
      case 1:
        newFlaggedString = "true";
        break;
    };
		
		//update the flagged
		record.set('flagged',        newFlagged);
		record.set('flagged_string', newFlaggedString);
		
		var scope = this;
		
    clearTimeout(this.timer['flagged']);
    this.timer['flagged'] = setTimeout(function() {
  		scope.showUpdating();
    
      // Update the record with the new type value
      record.update({
        flagged:        newFlagged,
        flagged_string: newFlaggedString
      },{
        url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
        success: scope.hideLoading,
        failure: function() {
          record.set('flagged', flagged);
          record.set('flagged_string', flaggedString);
          return Ext.Msg.alert('Error', 'Sorry, something went wrong. Please try again');
        }
      });
    }, 2000);
  },
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('tasks');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	/**
	 * Hides the loading animation from the view
	 */
	hideLoading: function() {
	  var controller = Kohive.OS.getController('tasks');
		var view = controller.indexWindow || null;
		
		if (view) {
		  view.hideLoading();
      // view.fireEvent('update-filter-bar', view.cachedStore);
		};
	},
	
	/**
	 * Refreshes the content in the view
	 */
	refreshView: function() {
		var controller = Kohive.OS.getController('tasks');
		var view = controller.indexWindow || null;
		
    if (view && view.store) view.store.load();
		if (view) view.hideLoading();
	},

	/**
	 * Creates a new combobox for gender
	 */
	assignedPicker: function(dataview, record, el, a, index, indexWindow) {
		var sibling = el.insertSibling({
			id:  'edit-assignedTo-field',
			cls: 'assigned_to-field'
		});
		
		el.enableDisplayMode();
		el.hide();
		
		var memberships = [[Ext.ux.Translator.translate('none'), null]];
    
    Ext.each(Kohive.OS.getCurrentHiveMemberships(), function(membership) {
      if (membership.get('state') == 'active') {
        memberships.push([membership.get('id_card').full_name, membership.get('id'), membership.get('id_card').nickname]);
      };
    }, this);    
    
    var membershipsStore = new Ext.data.SimpleStore({
      fields: ['alias', 'assignee_membership_id', 'nickname'],
      data:   memberships
    });
		
		var field = new Ext.form.ComboBox({
			renderTo:      'edit-assignedTo-field',
			name:          'assignee_membership_id',
			displayField:  'alias',
      valueField:    'assignee_membership_id',
			value:         record.get('assignee_membership_id'),
			width:         110,
			store:         membershipsStore,
			selectOnFocus: true,
      triggerAction: 'all',
      typeAhead:     true,
      emptyText:     '',
      mode:          'local',
			listeners: {
				scope    : this,
				'blur'   : function() {
					sibling.remove();
					el.show();
				},
				'select' : function(field, r) {
					record.set('assignee_nickname', r.get('nickname'));
					record.set('assignee_membership_id', r.get('assignee_membership_id'));
					
					sibling.remove();
					el.show();
					
					record.save({
						url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
						scope:   this,
						success: function() {
							
						},
						failure: function() {
							
						}
					});
				}
			}
		});
		
		var tempTask = new Ext.util.DelayedTask(function() {
		  field.focus();
			field.el.unselectable();
		}, this);
		tempTask.delay(100);
	},
	
	filesPicker: function(dataview, record, el, a, index, indexWindow) {
		var sibling = el.insertSibling({
			id:  'edit-files-field',
			cls: 'files-field'
		});
		
		el.enableDisplayMode();
		el.hide();
		
		var store = Kohive.OS.getController('tasks').files[Kohive.OS.getCurrentHiveId()];
		
		var field = new Ext.form.ComboBox({
			renderTo:        'edit-files-field',
			name:            'file_cabinet_id',
      hiddenName:      'file_cabinet_id',
      triggerAction:   'all',
      mode:            'local',
      displayField:    'title',
      valueField:      'id',
			selectOnFocus:   true,
			forceSelection:  true,
      enableKeyEvents: true,
			store:           store,
			value:           (record.get('file_cabinet_id') == 0) ? null : record.get('file_cabinet_id'),
			width:           110,
			listeners: {
				scope    : this,
				'blur'   : function() {
					sibling.remove();
					el.show();
				},
				'select' : function(field, r) {
				  //if the user selected none, do nothing
				  if (record.get('file_cabinet_id') == r.get('id')) return;
				  
					record.set('file_cabinet_id', (r.get('id')) ? r.get('id') : 0);
				  
					this.showUpdating();
					
					record.save({
						url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
						scope:   this,
						success: function(model, response) {
						  var task = Ext.decode(response.responseText);
						  record.set('file_cabinet_id', task.file_cabinet_id);
						  record.set('file_cabinet', task.file_cabinet);
						  record.set('title', "");
						  record.set('title', task.title);
						  
							sibling.remove();
							el.show();
							
							this.hideLoading();
						},
						failure: function() {
							this.showFailure();
						}
					});
				}
			}
		});
		
		var tempTask = new Ext.util.DelayedTask(function() {
		  field.focus();
			field.el.unselectable();
		}, this);
		tempTask.delay(100);
	},
	
	milestonePicker: function(dataview, record, el, a, index, indexWindow) {
		var sibling = el.insertSibling({
			id:  'edit-milestones-field',
			cls: 'milestones-field'
		});
		
		el.enableDisplayMode();
		el.hide();
		
		var store = Kohive.OS.getController('tasks').milestones[Kohive.OS.getCurrentHiveId()];
		
		var field = new Ext.form.ComboBox({
			renderTo:        'edit-milestones-field',
			name:            'milestone_id',
      hiddenName:      'milestone_id',
      triggerAction:   'all',
      mode:            'local',
      displayField:    'title',
      valueField:      'id',
			selectOnFocus:   true,
			forceSelection:  true,
      enableKeyEvents: true,
			store:           store,
			value:           (record.get('milestone_id') == 0) ? null : record.get('milestone_id'),
			width:           110,
			listeners: {
				scope    : this,
				'blur'   : function() {
					sibling.remove();
					el.show();
				},
				'select' : function(field, r) {
				  //if the user selected none, do nothing
				  if (record.get('milestone_id') == r.get('id')) return;
				  
					record.set('milestone_id', (r.get('id')) ? r.get('id') : 0);
				  
					this.showUpdating();
					
					record.save({
						url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
						scope:   this,
						success: function(model, response) {
						  var task = Ext.decode(response.responseText);
						  record.set('milestone_id', task.milestone_id);
						  record.set('milestone', task.milestone);
						  record.set('title', "");
						  record.set('title', task.title);
						  
							sibling.remove();
							el.show();
							
							this.hideLoading();
						},
						failure: function() {
							this.showFailure();
						}
					});
				}
			}
		});
		
		var tempTask = new Ext.util.DelayedTask(function() {
		  field.focus();
			field.el.unselectable();
		}, this);
		tempTask.delay(100);
	},
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('tasks');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	refreshView: function() {
		var controller = Kohive.OS.getController('tasks');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.store.load();
	},
	
	/**
	 * Creates a datepicker for the due date editable field
	 */
	duePicker: function(dataview, record, el, a, index, indexWindow) {
		var sibling = el.insertSibling({
			id:  'edit-due-field',
			cls: 'edit-due-field'
		});
		
		el.enableDisplayMode();
		el.hide();
		
		var dateField = new Ext.form.DateField({
			renderTo:   'edit-due-field',
			name:       'due',
			value:      (record.get('due_normal')) ? record.get('due_normal') : '',
      showToday:  false,
			width:      110,
			dontAddClickListener: true,
			format:     'd-m-Y', // TODO change to j-M-y when chronic can support it
			minValue:   new Date().format(Kohive.OS.getDefaultDateFormat()),
			listeners: {
				scope   : this,
				'focus' : function(field) {
					field.onTriggerClick();
				},
				'specialkey' : function(field) {
					var key = Ext.EventObject.getKey();
		      if (key === 13) this.saveDueDate(field, record, sibling, el);
				},
				'blur'  : function(field) {
					this.saveDueDate(field, record, sibling, el);
				}
			}
		});
		
		var tempTask = new Ext.util.DelayedTask(function() {
		  dateField.focus();
		}, this);
		tempTask.delay(100);
	},
	
	saveDueDate: function(field, record, sibling, el, dataview) {
		var dateOrg = (field.getValue()) ? field.getValue() : field.getRawValue();
		date = (dateOrg.format) ? dateOrg.format(Kohive.OS.getDefaultDateFormat()) : dateOrg;
		
		if (date) {
			record.set('due', date);
			record.set('due_date', date);
			record.set('overdue', (function() {
				if (!dateOrg.format) return;
				
				// Convert the due date into a proper JS date, and get the current date
				var dueDate = date;
				var currentDate = new Date();

				// Check if the due date is below or lower than the current date
				if (dueDate < currentDate) return true;

				return false;
			})());
		} else {
			record.set('due', '');
			record.set('due_date', '');
			record.set('overdue', false);
		};
		
		record.save({
			url:   '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
			scope: this,
			success: function() {
				sibling.remove();
				el.show();
				this.hideLoading();
			},
			failure: Ext.emptyFn
		});
	},
	
	/**
	 * 
	 */
	editTags: function(dataview, record, el, a, index, indexWindow) {
	  var sibling = el.insertSibling({
			id:  'edit-tags-field'
		});
		
		el.enableDisplayMode();
		el.hide();
	  
	  this.tagsField = new Ext.ux.form.SuperBoxSelect({
	    renderTo:        'edit-tags-field',
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      store:           indexWindow.tagsStore,
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        newitem: function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        },
        expand: function() {
          var list = this.tagsField.list;
          
          var tempTask = new Ext.util.DelayedTask(function() {
            list.setTop(list.getTop() + 3);
          }, this);
          tempTask.delay(200);
        },
        'enter-key': function(field) {
          this.saveTags(field, record, sibling, el);
        },
        blur: function(field) {
          this.saveTags(field, record, sibling, el);
				}
      }
		});
		
		this.tagsField.setWidth(sibling.getWidth());
    
    //format tags
    var tags = Ext.util.Format.trim(record.get('tag_list')).split(' ');
		
		var array = [];
		Ext.each(tags, function(t) {
		  array.push({
		    tag: t
		  });
		}, this);
		
		//set the tags value
    this.tagsField.setValueEx(array);
    
    this.tagsField.focus();
	},
	
	saveTags: function(field, record, sibling, el, dataview) {
		var val = Ext.util.Format.trim(this.tagsField.getValue()).split(',');
		
		if (val.length > 0) {
		  if (val.length == 1 && val[0] == "") {
		    record.set('tag_list', '');
  			record.set('tags', false);
  			this.tagsField.setValue('');
		  } else {
  		  var newArray = [];
		  
  		  Ext.each(val, function(a) {
  		    newArray.push({name:a});
  		  }, this);
		  
  			record.set('tag_list', val.join(' '));
  			record.set('tags', false);
  			record.set('tags', newArray);
  			this.tagsField.setValue(val);
  		};
		};
		
		this.showUpdating();
		
		record.save({
			url:   '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks/' + record.get('id'),
			scope: this,
			success: function() {
				this.tagsField.destroy();
				el.show();
				this.hideLoading();
			},
			failure: Ext.emptyFn
		});
	}
});

/**
 * @class Kohive.models.Video
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.Video", {
  fields:    [
		{name: 'id',                   type: 'int', virtual: true},
    {name: 'video_album_id',       type: 'int', virtual: true},
    {name: 'author_membership_id', type: 'int', virtual: true},
    {name: 'reference_id',    		 type: 'string'},
    {name: 'site',           			 type: 'string'},
    {name: 'guid',                 type: 'string'},
    {name: 'import_string',        type: 'string'},
    {name: 'title',                type: 'string'},
    {name: 'description',          type: 'string'},
    {name: 'created_at',           type: 'string', virtual: true},
    {name: 'updated_at',           type: 'string', virtual: true},
    {name: 'embed_code',           type: 'string', virtual: true},
    
    {
      name:    'author_nickname',
      type:    'string',
      virtual: true,
      convert: function(value, record) {
        var id = Kohive.OS.getCurrentHive().get('id');
        
        if (!id || !record.author_membership_id) return value;
        if (id == record.author_membership_id) return Ext.ux.Translator.translate('me');
        return value;
      }
    },
    
		{
			name: 'thumbnail_filename',
			virtual: true,
			convert: function(value, record) {
				return 'http://img.youtube.com/vi/' + record.reference_id + '/default.jpg';
			}
		},
		
    {
			name: 'tag_list',
			convert: function(value, record) {
				if (!value && record.tags.length > 0) {
					var res = "";
					Ext.each(record.tags, function(tag) {
					  res += tag.name + " ";
					}, this);
					return res;
				} else {
					return value;
				}
			}
		},
    {
      name: 'tags',
      virtual: true,
      convert: function(value, record) {
        if (value.length < 1) value = false;
        
        return value;
      }
    },
    
    {
      name: 'created_at_date',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.convertToDate(record.created_at, true);
      }
    },
    {
      name: 'id_card',
      virtual: true,
      convert: function(value, record) {
        return Kohive.OS.getIdCardFromMembershipId(record.author_membership_id);
      }
    },
    
		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  
  belongsTo: "VideoAlbum",
  hasMany:   "Comment",
  
  
  /**
	 * 
	 */
	editTags: function(dataview, record, el, a, index, indexWindow) {
	  var sibling = el.insertSibling({
			id:  'edit-tags-field'
		});
		
		el.enableDisplayMode();
		el.hide();
	  
	  this.tagsField = new Ext.ux.form.SuperBoxSelect({
	    renderTo:        'edit-tags-field',
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      store:           indexWindow.tagsStore,
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        newitem: function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        },
        expand: function() {
          var list = this.tagsField.list;
          
          var tempTask = new Ext.util.DelayedTask(function() {
            list.setTop(list.getTop() + 3);
          }, this);
          tempTask.delay(200);
        },
        'enter-key': function(field) {
          this.saveTags(field, record, sibling, el);
        },
        blur: function(field) {
          this.saveTags(field, record, sibling, el);
				}
      }
		});
		
		this.tagsField.setWidth(sibling.getWidth());
    
    //format tags
    var tags = Ext.util.Format.trim(record.get('tag_list')).split(' ');
		
		var array = [];
		Ext.each(tags, function(t) {
		  array.push({
		    tag: t
		  });
		}, this);
		
		//set the tags value
    this.tagsField.setValueEx(array);
    
    this.tagsField.focus();
	},
	
	saveTags: function(field, record, sibling, el, dataview) {
		var val = Ext.util.Format.trim(this.tagsField.getValue()).split(',');
		
		if (val.length > 0) {
		  if (val.length == 1 && val[0] == "") {
		    record.set('tag_list', '');
  			record.set('tags', false);
  			this.tagsField.setValue('');
		  } else {
  		  var newArray = [];
		  
  		  Ext.each(val, function(a) {
  		    newArray.push({name:a});
  		  }, this);
		  
  			record.set('tag_list', val.join(' '));
  			record.set('tags', false);
  			record.set('tags', newArray);
  			this.tagsField.setValue(val);
  		};
		};
		
		this.showUpdating();
		
		record.save({
		  url:   Kohive.OS.scopedUrl(String.format("video_albums/{0}/videos/{1}", record.get('video_album_id'), record.get('id'))),
			scope: this,
			success: function() {
				sibling.remove();
				el.show();
				this.hideLoading();
			},
			failure: Ext.emptyFn
		});
	},
	
	showUpdating: function() {
		var controller = Kohive.OS.getController('videos');
		var view = controller.indexWindow || null;
		
		if (view && view.store) view.showUpdating();
	},
	
	/**
	 * Hides the loading animation from the view
	 */
	hideLoading: function() {
	  var controller = Kohive.OS.getController('videos');
		var view = controller.indexWindow || null;
		
		if (view) {
		  view.hideLoading();
		  view.fireEvent('update-filter-bar', view.cachedStore);
		};
	},
	
	/**
	 * Refreshes the content in the view
	 */
	refreshView: function() {
		var controller = Kohive.OS.getController('videos');
		var view = controller.indexWindow || null;
		
    if (view && view.store) view.store.load();
		if (view) view.hideLoading();
	}
});

/**
 * @class Kohive.models.VideoAlbum
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.VideoAlbum", {
	modelName: 'video_album',
	jsonName:  'video_album',
	urlName:   'video_album',
	
  fields:    [
    {name: 'id',          type: 'int'},
    {name: 'author_membership_id', type: 'int'},
    {name: 'title',       type: 'string'},
    {name: 'hive_id',     type: 'int'},
    {name: 'type',        type: 'string'},
    {name: 'feed_url',    type: 'string'},
    {name: 'created_at',  type: 'string'},
    {name: 'updated_at',  type: 'string'},

		//editable properties
    {
      name: 'can_edit',
      virtual: true,
      convert: function(value, record) {
        //return true always until proper checking can be done
        return true;
        
				//author
				//mod/owner
				if (Kohive.OS.isAuthor(record)) return true;
				return false;
      }
    },
		{
      name: 'can_delete',
      virtual: true,
      convert: function(value, record) {
				//author
				//recipient
				//mod/owner
				if (Kohive.OS.isAuthor(record) || Kohive.OS.isHiveMod()) return true;
				return false;
      }
    }
  ],
  hasMany: "Video"
});

/**
 * @class Kohive.models.UrlInfo
 * @extends ExtMVC.Model
 */
ExtMVC.Model.define("Kohive.models.UrlInfo", {
  fields: [
    {name: 'address'}
  ]
});

/**
 * @class Kohive.controllers.AppsController
 * @extends ExtMVC.Controller
 * Manages applications on Kohive
 */
Kohive.controllers.AppsController = function() {
  // Get the OS and the controller
  var os         = ExtMVC.OS.getOS();
  var controller = this;
  
  Kohive.controllers.AppsController.superclass.constructor.call(this);
  
  /**
   * @action destroy 
   * Action which attempts to destroy an application
   */
  this.registerAction('destroy', function(app) {
    // Check if the application + application id has been set, as if they haven't we cannot
    // destroy the application
    if (!app || !app.id) { return; };
    
    // Create a new instance of an application with the correct Id
    var application = new Kohive.models.Application(app);
    
    // Create the function 
    var destroyFn = function(msg) {
      // Check if the user selected remove (yes btn in ext)
      if (msg == "yes") {
        application.destroy({
          url: '/memberships/' + os.getCurrentHive().get('id') + '/applications/' + app.id,
          success: function() {
            
          },
          failure: function(errors) {
            Ext.Msg.alert('delete_application', '306:' + Ext.ux.Translator.translate('something_wrong'));
          }
        });
      };
    };
    
    // Show the Ext.Msg to allow the user to either remove to keep the application
    return Ext.Msg.show({
      title:   Ext.ux.Translator.translate(''),
      msg:     Ext.ux.Translator.translate('delete_application_desc', { applcation_name: app.text || "" }),
      buttons: {yes: Ext.ux.Translator.translate('remove'), no: Ext.ux.Translator.translate('keep')},
      fn:      destroyFn
    });
  });
};

Ext.extend(Kohive.controllers.AppsController, ExtMVC.Controller);

ExtMVC.OS.getOS().registerController('apps', Kohive.controllers.AppsController);

Ext.ns('Kohive.views.apps');

/**
 * @class Kohive.controllers.KohiveController
 * @extends ExtMVC.Controller
 * Base Kohive Controller
 */
Kohive.controllers.KohiveController = Ext.extend(ExtMVC.Controller, {
  
  /**
   * @property viewCache
   * @type Object
   * An object which retains cached views per hive so that they can be shown again rather than
   * re-rendered
   */
  viewCache: {},
  
  /**
   * If a given view ID has already been rendered inside the current Hive, this will return it
   * @param {String} id The ID of the view to retrieve
   * @return {Ext.Component|Null} The cached view object, if found
   */
  getViewFromCache: function(id) {
    var cache = this.getCacheForCurrentHiveAndController();
    
    return cache[id];
  },
  
  /**
   * Caches a given view instance keyed by current hive.
   * @param {String} key The key to cache this component under (usually its viewname - e.g. 'index', 'new' etc)
   * @param {Ext.Component} component A component to cache within the current hive (usually a Kohive.Window)
   */
  addToViewCache: function(key, component) {
    var cache = this.getCacheForCurrentHiveAndController();
    
    cache[key] = component;
  },
  
  /**
   * Function description
   */
  saveViewsForCurrentHive: function() {
    var controllers = this.getCacheForCurrentHive();
    
    for (var c in controllers) {
      var views = controllers[c];
      
      for (var v in views) {
        var view = views[v];
        
        // The controller can have strange 'refresh' views (maybe others, too), which aren't valid views
        if (view) {
          view.relaunch = !view.hidden;
          view.hide();
        }
      }
    }
  },
  
  /**
   * For every view cached in the currently loaded hive, runs show()
   */
  relaunchViewsForCurrentHive: function() {
    var controllers = this.getCacheForCurrentHive();
    
    for (var c in controllers) {
      var views = controllers[c];
      
      for (var v in views) {
        if (views[v] != undefined && views[v].relaunch) views[v].show();
      }      
    }

  },
  
  /**
   * Internal method, returns the component cache for the current hive and creates it if not already present
   * @return {Object} The cache object for the current hive
   */
  getCacheForCurrentHive: function() {
    var membership = Kohive.OS.getCurrentHive();
    if (membership) {
      var hiveId = membership.get('hive_id');
      
      this.viewCache[hiveId] = this.viewCache[hiveId] || {};
      
      return this.viewCache[hiveId];
    } else return {};
  },
  
  /**
   * Internal method, returns the component cache for the current hive AND controller and creates it if not already present
   * @return {Object} The cache object for the current hive/controller combination
   */
  getCacheForCurrentHiveAndController: function() {
    var membership = Kohive.OS.getCurrentHive();
    if (membership) {
      var hiveId = membership.get('hive_id');
      
      this.viewCache[hiveId] = this.viewCache[hiveId] || {};
      this.viewCache[hiveId][this.name] = this.viewCache[hiveId][this.name] || {};
      
      return this.viewCache[hiveId][this.name];
    } else return {};
  },
  
  /**
   * Renders the view by calling the superclass. Also caches this view onto the current controller's cache, allowing us to relaunch
   * views when we switch hives.  By default, each view is cached with the viewName as the cache key.  To override this, provide a
   * viewCacheKey property in the config object and this will be used again.  Use this whenever you need to use the same view more 
   * than once.  e.g. to set a cache key for the edit screen of object with ID = 34 you might do this:
   * this.renderView('edit', {viewCacheKey: 'edit-34'})
   * @param {String} viewName The name of the view to render. This is translated into the appropriate view in this controller's view namespace
   * @param {Object} config A config object passed to the view constructor.  Provide a viewCacheKey to set a custom view cache key
   */
  renderView: function(viewName, config) {
    var config = config || {},
        key    = config.viewCacheKey || viewName,
        view   = this.getViewFromCache(key);
    
    if (view == undefined) {
      //call super and add to cache
      var view = Kohive.controllers.KohiveController.superclass.renderView.apply(this, arguments);
      this.addToViewCache(key, view);
    }
		
		if (view) view.show();
    
    return view;
  },
  
	/**
   * TODO: A hacky method to prepend the /memberships/:membership_id/ segment to the front of an AJAX
   * request url.  This really ought to be handled by the model, so factor it out of here when possible
   * @param {String} url The url to append to the memberships scope
   * @return {String} The fully scoped url
   */
  scopedUrl: function(url) {
    return String.format("/memberships/{0}/{1}", Kohive.OS.getCurrentHive().get('id'), url);
  }
});

(function() {
  var controller = Kohive.controllers.KohiveController,
      proto      = controller.prototype;
  
  Kohive.OS.on({
    scope: controller,
    'beforehiveload': function(previousHive) {
      proto.saveViewsForCurrentHive();
    },
    'hiveload': function(newHive) {
      proto.relaunchViewsForCurrentHive();
    }
  });
})();

/**
 * Kohive.controllers.HivesController
 * @extends ExtMVC.Controller
 * Listens to events on the hive controls, creates Hives as necessary
 */
Kohive.controllers.HivesController = function() {
  Kohive.controllers.HivesController.superclass.constructor.call(this, {
    viewsPackage: Kohive.controllers.HivesController.views
  });
  
  var os = ExtMVC.OS.getOS();
  var controller = this;
  
  this.registerViews({
    'hiveSummary':       Kohive.HiveSummary,
    'hiveOptions':       Kohive.HiveOptions,
    'hivePanel':         Kohive.views.hives.Index,
    'addHivePanel':      Kohive.views.hives.New,
    'addIntroHivePanel': Kohive.views.hives.NewIntro
  });
  
  this.addEvents(
    /**
     * @event beforehiveload
     * Fires before a Hive is loaded.  Return false from any listener to cancel the load
     * @param {Kohive.models.Hive} hive The Hive which wants to be loaded
     */
    'beforehiveload',
    
    /**
     * @event hiveload
     * Fires once a Hive load is complete
     * @param {Kohive.models.Hive} hive The Hive which has just been loaded
     */
    'hiveload',
    
    /**
     * @event beforecreatehive
     * Fires before a new Hive object is saved.  Return false to cancel creation
     * @param {Kohive.models.Hive} hive The hive about to be created
     */
    'beforecreatehive',
    
    /**
     * @event hivecreated
     * Fires after successful creation of a new hive
     * @param {Kohive.models.Hive} hive The newly created hive
     */
    'hivecreated',
    
    /**
     * @event createhivefailed
     * Fires if creation of a new hive failed for any reason.
     * Return false with a listener to provide your own handling of this event, otherwise
     * it defaults to showing an error message to the user
     * @param {Kohive.models.Hive} hive A Hive model object which could not be saved
     */
    'createhivefailed'
  );
  
  // Add events to the hivePanel
  this.handleEvent(os.hivePanel,   'new',     'new',         this);
  this.handleEvent(os.hiveOptions, 'accept',  'acceptHive',  this);
  this.handleEvent(os.hiveOptions, 'destroy', 'destroyHive', this);
  
	this.initListeners();
	
  // Refresh the hivePanel when you create a hive
  this.on('hivecreated', function(membership) {
    if (!membership) return;
    
    // Set the isNew property to true so it animates
    membership.data.hive.isNew = true;
    
    // Add the hive to the hive panel
    var btn = os.getHivePanel().add(membership);
  }, this);
  
  /**
   * @property hivePanelShown
   * @type Boolean
   * True if the hive panel has been already shown
   */
  this.hivePanelShown = false;
  
  /**
   * Displays the Add Hive panel, anchored above a specific Ext.Button.  Intended to be attached
   * to the click event of the Add Hive button on the HivePanel.
   * @action addHive
   * @param {Ext.Button} btn The Ext.Button to align the Add Hive panel over.  This will probably
   * always be the Add Hive button, sitting in the HivePanel.specials container
   */
  this.registerAction('new', function(btn) {
    var btn = btn || os.hivePanel.newButton;
    
    // Get the add hive panel if it has already been rendered
    var v = this.getRunningView('addHivePanel');
    
    var offset = [0, -45];

    if (!v) {
      //not rendered yet. Render now and show + align on render
      v = this.renderView('addHivePanel', {
        hidden: true,
        listeners: {
          'render': {
            fn: function() {
              var tempTask = new Ext.util.DelayedTask(function() {
                this.showHUD(offset);
              }, this); 
              
              tempTask.delay(10);
            }
          }
        }
      });
            
      //add save and cancel actions to the Add Hive Panel now that it has been created
      this.handleEvent(v, 'cancel', 'cancelAddHive', this);
      this.handleEvent(v, 'save',   'saveHive',      this);
    } else {
      v.show();
    };
  });
	
	/**
   * Displays the Add Hive panel, anchored above a specific Ext.Button.  Intended to be attached
   * to the click event of the Add Hive button on the HivePanel.
   * @action addHive
   * @param {Ext.Button} btn The Ext.Button to align the Add Hive panel over.  This will probably
   * always be the Add Hive button, sitting in the HivePanel.specials container
   */
  this.registerAction('new_intro', function(btn) {
    var btn = btn || os.hivePanel.newButton;
    
    // Get the add hive panel if it has already been rendered
    var v = this.getRunningView('addIntroHivePanel');
    
    var offset = [0, -45];

    if (!v) {
      //not rendered yet. Render now and show + align on render
      v = this.renderView('addIntroHivePanel', {
        hidden: true,
        listeners: {
          'render': {
            fn: function() {
              var tempTask = new Ext.util.DelayedTask(function() {
                this.showHUD(offset);
              }, this); 
              
              tempTask.delay(10);
            }
          }
        }
      });
      
			this.newIntro     = true;
			this.newIntroView = v;
			
      //add save and cancel actions to the Add Hive Panel now that it has been created
      this.handleEvent(v, 'cancel', 'cancelAddIntroHive', this);
      this.handleEvent(v, 'save',   'saveHive',           this);
    } else {
      v.show();
    };
  });
  
  /**
   * Hides the add hive panel
   * @action cancelAddHive
   * @param {Ext.Panel} addHivePanel The add hive panel which fired the event
   */
  this.registerAction('cancelAddHive', function(addHivePanel) {
    addHivePanel.hide();
  });

	/**
   * Hides the add hive panel
   * @action cancelAddHive
   * @param {Ext.Panel} addHivePanel The add hive panel which fired the event
   */
  this.registerAction('cancelAddIntroHive', function(addHivePanel) {
    addHivePanel.hide();
  });
  
  /**
   * Attempts to create and save a new Hive based on the values of the addHivePanel form
   * @action saveHive
   * @param {Ext.FormPanel} addHivePanel The add hive panel containing the add hive form
   */
  this.registerAction('saveHive', function(addHivePanel) {
		var hive = null;
		if (this.newIntro) {
			hive = new Kohive.models.Hive({
			  level: this.newIntroView.getLevel(),
				title: this.newIntroView.getValue()
			});
			
			this.newIntro = false;
		} else {
			//pro
			hive = new Kohive.models.Hive({
				level: 1,
				title: Ext.getCmp('new_hive_title').getValue()
			});
		};
		
    if (this.fireEvent('beforecreatehive', hive)) {
      hive.save({
        success: function(record, response) {
					Kohive.OS.loadSession();
					
          addHivePanel.onSuccess();
          
          // Get the data from the response and add it to the hive model
          var data = Ext.apply(record.data, Ext.decode(response.responseText));
          
          // Create a new membership with the hive data
          var membership = new Kohive.models.Membership(data);
					membership.firstHive = addHivePanel.firstHive === true;
          
          controller.fireEvent('hivecreated', membership);
					
					// //check if it is the firsthive, and if so, show the intro popup for inviting members
					if (addHivePanel.firstHive === true) {
						var tempTask = new Ext.util.DelayedTask(function() {
						  var membersController = Kohive.OS.getController('members');
							membersController.fireAction('new', membersController, [null, true]);
						}, this);
						tempTask.delay(5000);
					};
        },
        failure: function(modelObj, response) {
          // Get the data from the response and add it to the hive model
          var data = Ext.decode(response.responseText);
          
          controller.fireEvent('createhivefailed', hive);
          
          addHivePanel.onFailure(modelObj, data);
        },
        callback: function() {
          addHivePanel.endSave();
        },
        scope: this
      });
      
      // Call the startSave method which will disable the form
      addHivePanel.startSave();
    };
  });
  
  /**
   * Refreshes the contents of the global Hive Panel.
   * @action refresh
   * @param {Ext.Panel} hivePanel The global Hive Panel object
   */
  this.registerAction('refresh', function(extra) {
    var hivePanel = extra || os.getHivePanel();
    
    Kohive.models.Membership.findAll({
      url:      '/account/memberships',
      listeners:  {
        'load': {
          scope: this,
          fn: function(store, records) {
            var different = true;
            
						if (store.totalLength == 0) {
							ExtMVC.OS.getOS().hivePanel.positionHivePanel();
							this.fireAction('new_intro');
						};
						
            // Get the current hive and check if anything has changed
            var currentHive = os.getCurrentMemberships();
            if (currentHive) {
              different = controller.checkForDifferences(currentHive, store);
            };
            
            // Check if it is a different hive
            if (!different) {
              // Kohive.OS.setCurrentMemberships(store);
              return controller.fireEvent('refresh', this);
            };
            
            Kohive.OS.setCurrentMemberships(store);
            
            //first, remove any existing items
            if (hivePanel.items) hivePanel.items.each(function(item) { hivePanel.remove(item); }, this);
						
            //now add each Hive in turn, with a click listener on each
            for (var i = 0; i < records.length; i++) {
              var hive = records[i];
              var isNew = false;
              var currentHive = os.getCurrentHive() || null;
              if (currentHive && currentHive.data.id == hive.data.id) { isNew = true; };
              
              // Check if the personal_hive_id is set
              if (hive.data.personal_hive_id) {
                // var btn = hivePanel.add(hive, isNew);
                if (isNew) btn.toggle(true);
                
                // console.log(hive.data);
                // btn.on('click', function(button) {
                //   controller.fireAction('loadHive', controller, [null, button]);
                // }, hivePanel);
              } else {
                var btn = hivePanel.add(hive);
                if (isNew) btn.toggle(true);
              };
            };
            
            //if a hive is not currently loaded but one is in the location hash, load that now
            if (Kohive.OS.getCurrentHive() == null) Kohive.OS.loadHiveFromUrl();
            
            // Fade in all the views if the hive panel isnt already shown (first refresh)
            if (!this.hivePanelShown) {
              hivePanel.fireEvent('showview', this);
              // os.getQuickAppsBar().fireEvent(       'showview', this);
              // os.getWidgetBar().showView('t', {
              //   duration: 2,
              //   easing:   'easeOut'
              // });
              
              this.hivePanelShown = true;
            };
            
            // console.log(ExtMVC.OS.getOS().hivePanel.scroller);
            // console.log(ExtMVC.OS.getOS().hivePanel.latest);
            
            // scroll the hive panel
            // if (ExtMVC.OS.getOS().hivePanel.scroller) {
            //   ExtMVC.OS.getOS().hivePanel.scroll(0, true, true);
            // };
						
            // this.el.anchorTo(Ext.getBody(), 'b-b');
            controller.fireEvent('refresh', this);
          }
        }
      }
    });
  });
  
  /**
   * Accepts a hive invitation
   * @action acceptHive
   * @param {Kohive.HiveOptions} options The hive options bar
   */
  this.registerAction('acceptHive', function(options, membership) {
    try {
			if (!membership) var membership = options.btn.membership || null;
    } catch(e) {};
        
    // Check if the membership is null
    if (membership == null) return;
    
    // Set the state field to active
    var values = {
      "state": "active"
    };
    
    // Fire off the accept request method and override the success method
    membership.update(values, {
      success:  function() {
        controller.fireAction('refresh', this);
      },
      failure:  function() {
        Ext.Msg.alert(Ext.ux.Translator.translate('accept_hive'), '301:' + Ext.ux.Translator.translate('something_wrong'));
      }
    });
  });
  
  /**
   * Destroys/deletes a hive from the users hive list
   * @action destroyHive
   * @param {Kohive.HiveOptions} options The hive options bar
   */
  this.registerAction('destroyHive', function(options, membership, abtn) {
    try {
			if (!membership) var membership = options.btn.membership || null;
    } catch(e) {};
		
    // Check if the membership is null
    if (membership == null) return;
    
    var isPending = false;
    if (membership.data.state == "pending") isPending = true;
		
    // Will destroy/cancel an invite to a membership/hive
    var destroyFn = function(btn) {
      if (btn == 'yes') {
        // Get the btn of the hive which is being deleted/left and get the .center child,
        // this is where the text for the hive name is kept, keep a copy of it if we ever need
        // to restore it (on failure). then add a pending class so it fades out + is unselectable
        // and change the text to deleting
        var btn    = abtn || options.btn;
        var center = btn.el.child('.center');
        var text   = center.dom.innerHTML;
				
        btn.addClass('x-pending');
        center.update(Ext.ux.Translator.translate((isPending) ? 'declining' : 'leaving'));
        
        // Fire off the accept request method and override the success method
        membership.destroy({
          success: function() {
						Kohive.OS.loadSession();
            btn.animateRemoveHive();
						
            if (membership.data.id == Kohive.OS.getCurrentHive().data.id) {
              controller.fireAction('loadHive', controller, [Kohive.OS.getCurrentMemberships().data.items[0].data.hive_id]);
            };
          },
          failure: function(errors) {
            // something has wrong so lets put the hive back to the way it was
            center.update(text);
            btn.removeClass('x-pending');
            
            Ext.Msg.alert(Ext.ux.Translator.translate((isPending) ? 'decline_hive_invite' : 'leave_hive'), '302:' + Ext.ux.Translator.translate('something_wrong'));
          }
        });
      };
    };
    
		if (membership && abtn) return destroyFn('yes');
		
    // check if the hive is pending (cancel invite)
    if (isPending) {
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('decline_hive_invite_desc', { hive_title: options.hive.title }),
        buttons: {yes: Ext.ux.Translator.translate('decline_hive_invite'), no: Ext.ux.Translator.translate('cancel')},
        fn:      destroyFn
      });
    };
    
    // If it is an active hive, just destroy
    return Ext.Msg.show({
      title:   '',
      msg:     Ext.ux.Translator.translate('leave_hive_desc', { hive_title: options.hive.title }),
      buttons: {yes: Ext.ux.Translator.translate('leave_hive'), no: Ext.ux.Translator.translate('stay_in_hive')},
      fn:      destroyFn
    });
  });
  
  /**
   * Loads the Hive specified by the given Hive Id or Hive model
   * Other UI elements should listen to either beforehiveload or hiveload to update
   * themselves at the appropriate time.
   * @param {Kohive.models.Hive/String} hive The Hive object to load, or a unique Hive ID
   */
  this.registerAction('loadHive', function(id, button, animate) {
    // Kohive.OS.log('Stop pinging');
    // Kohive.OS.membersLoaded = false;
    // Kohive.OS.applicationsLoaded = false;
    
    var id = Math.abs(id) || null;
    var button = button || null;
    
    // Find the currently loaded hive, if one has been set
    var currentHive = os.getCurrentHive() || null;
    
    // If no hive is set then we must find one using the id
    var membership, hive;
    if (button) {
      membership = button.membership || null;
      hive = membership.data.hive || null;
    } else {
      // Loop through each of the hive memberships and find one with the correct hive id
      Ext.each(Kohive.OS.getCurrentMemberships().data.items, function(m) {
        if (id == m.data.hive.id) {
          membership = m;
          hive = membership.data.hive || null;
        };
      }, this);
    };
    
    // Now we have an instance of a hive model, we can go ahead and activate the hive
    if (membership && this.fireEvent('beforehiveload', membership)) {
      // Check if the user is trying to re-load the current hive
      if (currentHive && currentHive.data.hive.id == hive.id) return;
      
      // Toggle the hive
      if (button) {
        button.toggle();
      } else {
        try {
          Kohive.OS.getHivePanel().items.find(function(e) {
            if (e.hive.id == hive.id) {
              e.toggle();
            };
          });
        } catch(e) {};
      };
      
      // Add the history token
      var hash = window.location.hash;
      hash = hash.substring(1);
      hash = hash.split('/')[0];
      hash = Math.abs(hash);
      
      // Check if the current hash is the same as the new one
      if (hash != membership.data.hive.id) {
        Ext.History.add(membership.get('hive').id);
      };
      
      // Set the current hive to the newly loaded hive
      os.setCurrentHive(membership);
			
			var user       = Kohive.OS.getCurrentUser();
			var controller = Kohive.OS.getController('users');
			if (user && controller && user.get('default_hive_id') != membership.get('hive').id) {
			  controller.update.defer(2000, controller, [user, {
				  default_hive_id: membership.get('hive').id
			  }]);
			}
    };
  });
  
  // load up the hives onLoad
  this.fireAction('refresh');
};
Ext.extend(Kohive.controllers.HivesController, ExtMVC.Controller, {
  /**
   * 
   * @param {Kohive.models.Membership} currentHive The current memberships
   * @param {Kohive.models.Membership} newHive The new memberships
   */
  checkForDifferences: function(currentHive, newHive) {
    // Create an array for each of the memberships
    var currentHiveArray = [];
    var newHiveArray     = [];
    
    // check if the length is different
    if (currentHive.data.items.length != newHive.data.items.length) { return true; };
    
    Ext.each(currentHive.data.items, function(item) {
      currentHiveArray.push(Ext.encode(item.data));
    }, this);
    
    Ext.each(newHive.data.items, function(item) {
      newHiveArray.push(Ext.encode(item.data));
    }, this);
    
    var different = false;
    
    Ext.each(currentHiveArray, function(item, index) {
      if (item != newHiveArray[index]) { return different = true; };
    }, this);
    
    return (different) ? true : false;
  },
	
	initListeners: function() {
		this.on({
			scope            : this,
			'beforehiveload' : this.onBeforeHiveLoad
		});
	},
	
	/**
	 * Called before the hive is loaded. Hides the application launcher and starts the hive loading.
	 */
	onBeforeHiveLoad: function(controller, button) {
		// Kohive.OS.getApplicationLauncher().hideView();
		// this.fireAction('loadHive', this, [null, button, true]);
	}
});

ExtMVC.OS.getOS().registerController('hives', Kohive.controllers.HivesController);

Ext.ns('Kohive.views.hives');

/**
 * Kohive.controllers.MembersController
 * @extends ExtMVC.Controller
 * Manages invitation of Members
 */
Kohive.controllers.MembersController = function() {
  Kohive.controllers.MembersController.superclass.constructor.call(this, {
    viewsPackage: Kohive.controllers.MembersController.views
  });
  
  var os = ExtMVC.OS.getOS();
  var controller = this;
  
  this.registerViews({
    'memberSummary':  Kohive.MemberSummary,
    'memberOptions':  Kohive.MemberOptions,
    'memberPanel':    Kohive.MemberPanel,
    'addMemberPanel': Kohive.views.members.New
  });
  
  this.addEvents(    
    /**
     * @event beforecreatemember
     * Fires before a new Member object is saved.  Return false to cancel creation
     * @param {Kohive.models.Member} hive The Member about to be created
     */
    'beforecreatemember',
    
    /**
     * @event membercreated
     * Fires after successful creation of a new Member
     * @param {Kohive.models.Member} Member The newly created Member
     */
    'membercreated',
    
    /**
     * @event creatememberfailed
     * Fires if creation of a new Member failed for any reason.
     * Return false with a listener to provide your own handling of this event, otherwise
     * it defaults to showing an error message to the user
     * @param {Kohive.models.Member} hive A Member model object which could not be saved
     */
    'creatememberfailed',
    
    /**
     * @event membersloaded
     * Fires when the members are loaded for a hive
     */
    'membersloaded'
  );
  
  //Add events to be handled
  this.handleEvent(os.memberPanel,   'new',           'new',          this);
  
  this.handleEvent(os.memberOptions, 'acceptmember', 'acceptMember',  this);
  this.handleEvent(os.memberOptions, 'rejectmember', 'rejectMember', this);
  this.handleEvent(os.memberOptions, 'cancelmember', 'cancelMember', this);
  
  this.handleEvent(os.memberOptions, 'kickmember',   'destroyMember', this);
	
	this.initListeners();
	
  /**
   * Displays the Add Member panel, anchored above a specific Ext.Button.  Intended to be attached
   * to the click event of the Add Member button on the MemberPanel.
   * @action addMember
   * @param {Ext.Button} btn The Ext.Button to align the Add Member panel over.  This will probably
   * always be the Add Member button, sitting in the MemberPanel.specials container
   */
  this.registerAction('new', function(btn, firstHive) {
    var btn = Kohive.OS.getMemberPanel().newButton;
    
    var v = this.getRunningView('addMemberPanel');
    
    var offset = [0, -15];
    
    if (!v) {
      //not rendered yet. Render now and show + align on render
      v = this.renderView('addMemberPanel', {
        hidden: true,
				firstHive: firstHive,
        listeners: {
          'render': {
            fn: function() {
              this.showHUD(offset);
            }
          }
        }
      });
			
			v.hide();
      
      this.handleEvent(v, 'cancel',        'cancelAddMember', this);
      this.handleEvent(v, 'save',          'addMember',       this);
      this.handleEvent(v, 'find-contacts', 'findContacts',    this);
    } else {
      v.showHUD(offset);
    };
  });
  
  /**
   * @action addMember 
   * Invites or requests a new member into a hive
   */
  this.registerAction('addMember', function(addMemberPanel) {
    if (!addMemberPanel) return;
    
    // Call the startSave method which disables the form
    addMemberPanel.startSave();
    
    // Get the values of the form
    var values = {
      'email':   Ext.get('new_member_email').getValue(),
      'hive_id': os.getCurrentHive().data.hive.id
    };
    
    // Create a new membership from the above values
    var membership = new Kohive.models.Membership(values);
    
    // Attempt to save the membership to the server
    membership.save({
      success: function() {
        // Call the onsuccess method when it succeeds and reload the memberslist
        addMemberPanel.onSuccess();
        controller.fireAction('loadMembers');
      },
      failure: function(modelObj) {
        // Call the onfailure method when there is a problem and pass it the errors
        addMemberPanel.onFailure(modelObj);
      },
      callback: function() {
        // Call the endsave method once the response comes back
        addMemberPanel.endSave();
      }
    });
  });
	
	/**
	 * @action findContacts
	 * 
	 * @param {Ext.Panel} addMemberPanel The add member panel
	 * @param {String} email The email to use
	 * @param {String} service The service to use
	 */
	this.registerAction('findContacts', function(email, password, service) {
		var contact = {
			service:  service,
			login:    email,
			password: password
		};
		
		//dispatch the invite controller
		Kohive.OS.dispatch({
			controller: 'invite',
			action:     'index'
		}, null, [contact]);
	});
  
	this.registerAction('sendMessage', function(membership, el) {
		this.createNewMessageBubble(membership, el);
	});
	
	this.registerAction('loadMessages', function() {
		if (this.fireEvent('before-ping') === false) return;
		
		//get the messages
		var msgs = Kohive.models.Message.findAll({
			url: Kohive.OS.scopedUrl('messages'),
			listeners: {
				scope  : this,
				'load' : function(store) {
					//now the messages have loaded, show the first one and mark it as read
					var msg = store.getAt(0);
					if (!msg) return Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
					
					//mark the message as read
					msg.update({
						read: true
					}, {
						url: '/memberships/' + msg.get('assignee_membership_id') + '/messages/' + msg.get('id'),
						failure: function() {
							Ext.Msg.alert('', '401:' + Ext.ux.Translator.translate('something_wrong'));
							Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
						}
					});
					
					//show the message
					//get the member button
					var panel = Kohive.OS.getMemberPanel(),
					    nodes = panel.getNodes();
					
					if (msg.get('hive_id') == Kohive.OS.getCurrentHiveId()) {
						Ext.each(nodes, function(node) {
						  var record = panel.getRecord(node);

							//check what the author_membership_id is
							if (record.get('id') == msg.get('author_membership_id')) {
								//get the el
								var el = Ext.get(node);

								//make the node
								this.createMessageBubble(el, msg);
							} else {
								Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
							};
						}, this);
					} else {
						//find the hive to anchor to it
						var el = null;
						Kohive.OS.getHivePanel().items.each(function(item) {
							if (item.hive.id == msg.get('hive_id')) el = item.el;
						}, this);
						
						if (!el) return Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
						
						//make the node
						this.createMessageBubble(el, msg, true);
					};
				}
			}
		});
	});
	
  /**
   * Attempts to accept a pending member request.
   * @action acceptInvite
   * @param {Kohive.views.members.Button} btn The member button object
   */
  this.registerAction('acceptMember', function(options) {
    var member = options.record;
    if (member == null) return;
    
    // Fire off the accept request method and override the success method
    member.update(values, {
      url:      this.getAcceptUrl(),
      success:  function() {
        controller.fireAction('loadMembers');
      },
      failure:  function() {
        Ext.Msg.alert('Accept Invite', '303:' + Ext.ux.Translator.translate('something_wrong'));
      }
    });
  });
  
  /**
   * Attempts to accept a pending member request.
   * @action inviteMember
   * @param {Kohive.views.members.Button} btn The member button object
   */
  this.registerAction('inviteMember', function(member, hive, btn) {
    if (!member || !hive) return;
    
    // Get the values of the form
    var values = {
      'id_card_id': member.get('id_card_id'),
      'hive_id':    hive.id
    };
    
    // Create a new membership from the above values
    var membership = new Kohive.models.Membership(values);
    
    // Attempt to save the membership to the server
    membership.save({
      success: function() {
        // Call the onsuccess method when it succeeds and reload the memberslist
        // controller.fireAction('loadMembers');
      },
      failure: function(modelObj) {
				var isOwner = Kohive.OS.getCurrentUser().get('id') == hive.owner_id;
				if (!isOwner) {
	        Ext.Msg.alert('', Ext.ux.Translator.translate('add_member_max_reached_member'));
				} else {
	        Ext.Msg.alert('', Ext.ux.Translator.translate('add_member_max_reached_owner'));
				};
      },
      callback: function() {
        // Call the endsave method once the response comes back
        // addMemberPanel.endSave();
				btn.removeClass('add');
      }
    });
  });
  
  /**
   * Rejects a pending member invite.
   * @action rejectMember
   * @param {Kohive.views.members.Button} btn The member button object
   */
  this.registerAction('rejectMember', function(options) {
    var member = options.record;
    if (member == null) return;
    
    var rejectFn = function(btn) {
      if (btn == 'yes') {
        // Fire off the accept request method and override the success method
        member.update({}, {
          url:      member.getRejectUrl(),
          success:  function() {
            controller.fireAction('loadMembers');
          },
          failure:  function() {
            Ext.Msg.alert('Reject Invite', '304:' + Ext.ux.Translator.translate('something_wrong'));
          }
        });
      };
    };
    
    return Ext.Msg.show({
      title:   '',
      msg:     Ext.ux.Translator.translate('reject_inivitation_desc', {
        full_name:  member.get('id_card').full_name || member.get('id_card').email,
        hive_title: Kohive.OS.getCurrentHive().get('title')
      }),
      buttons: {yes: Ext.ux.Translator.translate('decline'), no: Ext.ux.Translator.translate('cancel')},
      fn:      rejectFn
    });
  });
  
  /**
   * Cancels a pending member invite.
   * @action cancelMember
   * @param {Kohive.views.members.Button} btn The member button object
   */
  this.registerAction('cancelMember', function(options) {
		var member = options.record;
    if (!member) return;
    
    var cancelFn = function(btn) {
      if (btn == 'yes') {
        // Fire off the accept request method and override the success method
        member.destroy({
          success:  function() {
            controller.fireAction('loadMembers');
          },
          failure:  function() {
            Ext.Msg.alert('Cancel Invite', '305:' + Ext.ux.Translator.translate('something_wrong'));
          }
        });
      };
    };
    
    return Ext.Msg.show({
      title:   '',
      msg:     Ext.ux.Translator.translate('cancel_inivitation_desc', {
        full_name:  member.get('id_card').full_name || member.get('id_card').email
      }),
      buttons: {yes: Ext.ux.Translator.translate('uninvite'), no: Ext.ux.Translator.translate('keep')},
      fn:      cancelFn
    });
  });
  
  /**
   * Attempts to kick a member from a hive
   * @action destroyMember
   * @param {Kohive.views.members.Button} btn The member button object
   */
  this.registerAction('destroyMember', function(options) {
    var member = options.record || null;
    if (member == null) { return; };
    
    var destroyFn = function(btn) {
      if (btn == 'yes') {
        // Fire off the accept request method and override the success method
        member.destroy({
          success: function() {
            // TODO animate
            controller.fireAction('loadMembers');
          },
          failure: function() {
            // if the member is yourself, display a different message
            if (member.get('user_id') == Kohive.OS.getCurrentUser().get('id')) {
              return Ext.Msg.alert(Ext.ux.Translator.translate('leave_hive'), '317:' + Ext.ux.Translator.translate('something_wrong'));
            };
            
            Ext.Msg.alert(Ext.ux.Translator.translate('kick_member'), 'Sorry, we could not kick this member. Please try again.');
          }
        });
      };
    };
    
    // if the member is yourself, display a different message
    if (member.data.id_card.user_id == Kohive.OS.getCurrentUser().data.id) {
      return Ext.Msg.show({
        title:   Ext.ux.Translator.translate('leave_hive'),
        msg:     Ext.ux.Translator.translate('leave_hive_desc', { hive_title: Kohive.OS.getCurrentHive().data.hive.title }),
        buttons: {yes: Ext.ux.Translator.translate('leave'), no: Ext.ux.Translator.translate('stay')},
        fn:      destroyFn
      });
    };
    
    return Ext.Msg.show({
      title:   Ext.ux.Translator.translate('kick_member'),
      msg:     Ext.ux.Translator.translate('kick_member_desc', { full_name: member.data.id_card.nickname || member.data.id_card.email }),
      buttons: {yes: Ext.ux.Translator.translate('kick_out'), no: Ext.ux.Translator.translate('keep')},
      fn:      destroyFn
    });
  });
  
  /**
   * Hides the add member panel
   * @action cancelAddMember
   * @param {Ext.Panel} addMemberPanel The add member panel which fired the event
   */
  this.registerAction('cancelAddMember', function(addMemberPanel) {
    addMemberPanel.hide();
  });
  
	/**
	 * Hides the member panel form view
	 */
	this.registerAction('beforeLoadMembers', function() {
		var memberPanel = memberPanel || Kohive.OS.getMemberPanel();
		
		Kohive.OS.getApplicationLauncher().hideView();
		memberPanel.hideView();
	});
	
  /**
   * Refreshes the contents of the global Member Panel.  
   * @param {Ext.Panel} memberPanel The global Member Panel object
   * @param {Boolean} force True if you want to skip check differences
   */
  this.registerAction('loadMembers', function(memberPanel) {
    if (!Kohive.OS.getCurrentHive()) return;
    
    var memberPanel = memberPanel || Kohive.OS.getMemberPanel();
    
    Kohive.models.Membership.findAll({
      url: '/hives/' + Kohive.OS.getCurrentHiveId() + '/memberships',
      listeners: {
        'load': {
          scope: this,
          fn: function(store, records, options) {
            Kohive.OS.log('Hive Memberships Store: loaded');
						Kohive.OS.getController('members').loaded = true;
						Kohive.OS.showMembersAndApplications(memberPanel, null, store);
            
						var memberships = [];
						store.each(function(record) {
							memberships.push(record);
						}, this);
						Kohive.OS.setCurrentHiveMemberships(memberships);
						
            // fire off the members loaded event
            // Kohive.OS.membersLoaded = true;
            memberPanel.fireEvent('membersloaded', this);
          }
        }
      }
    });
  });
};
Ext.extend(Kohive.controllers.MembersController, ExtMVC.Controller, {
	loaded: false,
	
	/**
	 * 
	 */
	initListeners: function() {
		Kohive.OS.getController('idcards').on({
			scope                 : this,
			'update'              : this.fireAction.createDelegate(this, ['loadMembers', this, [null, true]]),
			'hive-id-card-update' : this.fireAction.createDelegate(this, ['loadMembers', this, [null, true]])
		});
		
		Kohive.OS.on({
			scope            : this,
			'beforehiveload' : this.fireAction.createDelegate(this, ['beforeLoadMembers', this]),
			'hiveload'       : this.fireAction.createDelegate(this, ['loadMembers', this])
		});
	},
	
	createNewMessageBubble: function(membership, el) {
		Kohive.OS.on('before-ping', Kohive.OS.returnFalse, Kohive.OS);
		
		var bubble = new Ext.ux.HUD({
			anchorToEl:  el,
      title:       Ext.ux.Translator.translate('send_message_to_x', { assignee: membership.get('id_card').nickname }),
			width:       200,
			items: [
				{
					layout: 'column',
					items: [
						{
							id:         'hud_new_messages_field',
							bodyStyle:  'margin:1px;',
							xtype:      'field',
							allowBlank: false,
							width:      160
						},
						{
							bodyStyle: 'text-align:right;background:transparent;border:0;padding-left:3px;',
							items: [
								new Kohive.IconButton({
									id:       'hud_new_messages_icon_add',
							    iconCls:  'send'
							  })
							]
						}
					]
				}
			],
      cls:         'message',
      clickListener: function(e) {
        if (!e.getTarget('.x-hud-wrapper')) {
					this.destroy();
					Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
				};
      },
			startSave: function() {
		    this.disable();
		    this.saving = true;

		    // Find the form mask and create a loading animation child within it
		    var mask = this.el.child('.ext-el-mask');
		    if (mask) {
		      mask.setHeight(this.el.getHeight());
		      mask.setWidth(this.el.getWidth() - 3);
		      mask.anchorTo(this.el, 'b-b', [1, -1]);

		      this.loadingContainer = mask.createChild({
		        cls: 'x-loading'
		      });

		      this.loadingContainer.anchorTo(mask, 'c-c', [0, -3]);
		    };
		  },
		  endSave: function() {
		    if (this.loadingContainer) this.loadingContainer.remove();

		    this.enable();
		    this.saving = false;
		  }
		});
		
		//render the hud
		bubble.render();
		
		//add icon
		var field = Ext.getCmp('hud_new_messages_field');
		var icon = Ext.get('hud_new_messages_icon_add');
		icon.enableDisplayMode();
		
		var submitFn = function() {
			var field = Ext.getCmp('hud_new_messages_field');
			var value = field.getValue();
			
			//check if the field is valid
			if (field.isValid()) {
				field.disable();
				bubble.startSave();
				
				var message = new Kohive.models.Message({
					body:                   value,
					read:                   false,
					hive_id:                Kohive.OS.getCurrentHiveId(),
					author_membership_id:   Kohive.OS.getCurrentHive().get('id'),
					assignee_membership_id: membership.get('id')
				});
				
				message.save({
					url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/messages',
					callback: function() {
						bubble.endSave();
					},
					success: function() {
						bubble.destroy();
						Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
					},
					failure: function() {
						field.enable();
					}
				});
			};
		};
		
		field.on('specialkey', function(e) {
			var key = Ext.EventObject.getKey();
      if (key === 13) submitFn();
		}, this);
		icon.on('click', submitFn, this);
		
		//hide the hud
		bubble.hide();
		
		//show the hud
		bubble.showHUD([0, -15], 'b-t', 1, function() {
      bubble.positionPoker([0, 0], 't-b');
			
			Ext.get('hud_new_messages_field').focus();
    }, true);
	},
	
	createMessageBubble: function(el, msg, hive) {
		Kohive.OS.on('before-ping', Kohive.OS.returnFalse, Kohive.OS);
		
		var bubble = new Ext.ux.HUD({
			anchorToEl:  el,
      title:       Ext.ux.Translator.translate('message_from_x', { assignee: msg.get('author_nickname') }),
			width:       200,
			items: [
				{
					id:   'hud_messages_body',
					html: msg.get('body')
				},
				{
					id:     'hud_messages_column',
					layout: 'column',
					items: [
						{
							id:         'hud_messages_field',
							bodyStyle:  'margin:1px;',
							xtype:      'field',
							allowBlank: false,
							width:      160
						},
						{
							bodyStyle: 'text-align:right;background:transparent;border:0;padding-left:3px;',
							items: [
								new Kohive.IconButton({
									id:       'hud_messages_iconb',
							    iconCls:  'send'
							  })
							]
						}
					]
				},
				{
					bodyStyle: 'text-align:right;background:transparent;padding-top:5px;',
					items: [
						new Kohive.IconButton({
							id:       'hud_messages_icon',
					    iconCls:  'reply'
					  })
					]
				}
			],
      cls:         (hive === true) ? 'add-hive message' : 'message',
			pokerHeight: 44,
      clickListener: function(e) {
        if (!e.getTarget('.x-hud-wrapper')) {
					this.destroy();
					Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
				};
      },
			startSave: function() {
		    this.disable();
		    this.saving = true;

		    // Find the form mask and create a loading animation child within it
		    var mask = this.el.child('.ext-el-mask');
		    if (mask) {
		      mask.setHeight(this.el.getHeight() - 15);
		      mask.setWidth(this.el.getWidth() - 3);
		      mask.anchorTo(this.el, 't-t', [1, -1]);

		      this.loadingContainer = mask.createChild({
		        cls: 'x-loading'
		      });

		      this.loadingContainer.anchorTo(mask, 'c-c', [0, -3]);
		    };
		  },
		  endSave: function() {
		    if (this.loadingContainer) this.loadingContainer.remove();

		    this.enable();
		    this.saving = false;
		  }
		});
		
		//render the hud
		bubble.render();
		
		//field
		var field = Ext.get('hud_messages_field');
		field.enableDisplayMode();
		
		var columns = Ext.get('hud_messages_column');
		columns.enableDisplayMode();
		columns.hide();
		
		//add icon
		var icon = Ext.get('hud_messages_icon');
		var iconb = Ext.get('hud_messages_iconb');
		icon.enableDisplayMode();
		iconb.enableDisplayMode();
		
		var submitFn = function() {
			var field = Ext.getCmp('hud_messages_field');
			var value = field.getValue();
			
			//check if the field is valid
			if (field.isValid()) {
				field.disable();
				bubble.startSave();
				
				var message = new Kohive.models.Message({
					body:                   value,
					read:                   false,
					hive_id:                msg.get('hive_id'),
					author_membership_id:   msg.get('assignee_membership_id'),
					assignee_membership_id: msg.get('author_membership_id')
				});
				
				message.save({
					url: '/memberships/' + msg.get('assignee_membership_id') + '/messages',
					callback: function() {
						bubble.endSave();
						Kohive.OS.un('before-ping', Kohive.OS.returnFalse, Kohive.OS);
					},
					success: function() {
						bubble.destroy();
					},
					failure: function() {
						field.enable();
					}
				});
			};
		};
		
		var fielda = Ext.getCmp('hud_messages_field');
		fielda.on('specialkey', function(e) {
			var key = Ext.EventObject.getKey();
      if (key === 13) submitFn();
		}, this);
		
		iconb.on('click', submitFn, this);
		
		//body
		var body = Ext.get('hud_messages_body');
		body.enableDisplayMode();
		
		var bodyHeight  = body.getHeight();
		var fieldHeight = field.getHeight();
		
		var difference = bodyHeight - fieldHeight;
		
		//btn
		var btn = Ext.get('hud_messages_icon');
		btn.on('click', function() {
			if (columns.isVisible()) {
				icon.show();
				body.show();
				columns.hide();
				
				bubble.setTitle(Ext.ux.Translator.translate('message_from_x', { assignee: msg.get('author_nickname') }));
				
				bubble.el.setTop(bubble.el.getTop() - difference);
			} else {
				icon.hide();
				body.hide();
				columns.show();
				
				field.focus();
				
				bubble.setTitle(Ext.ux.Translator.translate('send_message_to_x', { assignee: msg.get('author_nickname') }));
				
				bubble.el.setTop(bubble.el.getTop() + difference);
			};
		}, this);
		
		//hide the hud
		bubble.hide();
		
		//show the hud
		bubble.showHUD((hive === true) ? [0, -50] : [0, -15], 'b-t', 1, function() {
      bubble.positionPoker([0, 0], 't-b');
    }, true);
		
		return bubble;
	}
});

ExtMVC.OS.getOS().registerController('members', Kohive.controllers.MembersController);

Ext.ns('Kohive.views.members');

/**
 * Kohive.app.BoardPostsController
 * @extends ExtMVC.Controller
 * Manages the addition and removal of bookmarks
 */
Kohive.app.BoardPostsController = Ext.extend(Kohive.controllers.KohiveController, {
  name:  'boardposts',
	title: Ext.ux.Translator.translate('app_board_posts'),
	  
  constructor: function(config) {
    var config = config || {};

    // Get the OS and the controller
    var os         = ExtMVC.OS.getOS();
    var controller = this;

    Kohive.app.BoardPostsController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.boardposts
    });

    this.actsAsKohiveController(Kohive.models.BoardPost);

    /**
     * @action destroy 
     * Deletes a given model instance
     * We need to overwrite the default one for this controller as it doesn't seem to use the
     * correct url (with _)
     */
    this.registerAction('destroy', function(record, store, contentEl) {
      var id = record.get('id');
      if (id) {
        var u = new this.model({id: id});
        u.destroy({
          scope:   this,
          url:     this.destroyUrl(id),
          success: this.onDestroySuccess.createDelegate(this, [store, contentEl]),
          failure: this.onDestroyFailure
        });
      };
    });
  },
  
  /**
   * Renders the main window
   */
  index: function() {
    this.renderView('index', {
			
		});
  },
  
  /**
   * Generates the destroy Url for a board post record using the saveUrl() method
   */
  destroyUrl: function(id) {
    return this.saveUrl() + '/' + id;
  }
});

ExtMVC.OS.getOS().registerController('boardposts', Kohive.app.BoardPostsController);

Ext.ns('Kohive.views.boardposts');

/**
 * Kohive.app.BlogsController
 * @extends ExtMVC.Controller
 * Manages the addition and removal of blogs
 */
Kohive.app.BlogsController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'blogs',
	title: Ext.ux.Translator.translate('app_blog'),
  
  constructor: function(config) {
    var config = config || {};

    // Get the OS and the controller
    var os         = ExtMVC.OS.getOS();
    var controller = this;

    Kohive.app.BlogsController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.blogs
    });

    this.actsAsKohiveController(Kohive.models.Blog);
  }
});

ExtMVC.OS.getOS().registerController('blogs', Kohive.app.BlogsController);

Ext.ns('Kohive.views.blogs');

/**
 * Kohive.app.BookmarksController
 * @extends ExtMVC.Controller
 * Manages the addition and removal of bookmarks
 */
Kohive.app.BookmarksController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'bookmarks',
  
  constructor: function(config) {
    var config = config || {};

    // Get the OS and the controller
    var os         = ExtMVC.OS.getOS();
    var controller = this;

    Kohive.app.BookmarksController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.bookmarks
    });

    this.actsAsKohiveController(Kohive.models.Bookmark);
  }
});

ExtMVC.OS.getOS().registerController('bookmarks', Kohive.app.BookmarksController);

Ext.ns('Kohive.views.bookmarks');

// /**
//  * Kohive.app.BookmarksController
//  * @extends ExtMVC.Controller
//  * Manages the addition and removal of bookmarks
//  */
// Kohive.app.BookmarksController = function() {
//   Kohive.app.BookmarksController.superclass.constructor.call(this, {
//     viewsPackage: Kohive.app.Bookmarks.views
//   });
//   
//   var os = ExtMVC.OS.getOS();
//   var controller = this;
//   
//   this.registerViews({
//     'index': Kohive.app.Bookmarks.views.Index
//   });
//   
//   /**
//    * @action index 
//    * Creates the Bookmarks window, listens to deleteBookmark event.
//    */
//   this.registerAction('index', function(config) {
//     var config = config || {};
//     
//     this.store = Kohive.models.Bookmark.findAll({
//       listeners:  {
//         'load': {
//           scope: this,
//           fn: function(store, records) {
//             this.indexWindow.cachedRecords = records;
//           }
//         }
//       }
//     });
//     
//     this.indexWindow = this.renderView('index', Ext.applyIf(config, {
//       store: this.store,
//       listeners: {
//         'create': {
//           scope: this,
//           fn:    function() { this.fireAction('create', this, arguments); }
//         },
//         'update': {
//           scope: this,
//           fn:    function(){ this.fireAction('update', this, arguments); }
//         },
//         'refresh':   this.fireAction.createDelegate(this, ['refresh']),
//         'toggleall': this.fireAction.createDelegate(this, ['toggleall'])
//       }
//     }));
//     
//     this.indexWindow.show();
//     
//     this.indexWindow.on('action-delete', controller.destroy, controller);
//     
//     /**
//      * Define events for 
//      */
//     this.indexWindow.on('action-goto', controller.gotoBookmark,   controller);
//     
//     /**
//      * Define the editable fields
//      */
//     this.indexWindow.on('action-edit-title', controller.delegateFieldEdit, controller);
//     this.indexWindow.on('action-edit-notes', controller.delegateFieldEdit, controller);
//     this.indexWindow.on('action-edit-url',   controller.delegateFieldEdit, controller);
//   });
//   
//   /**
//    * @action create 
//    * Creates a new Bookmark object, saves it
//    */
//   this.registerAction('create', function(form) {
//     // Check if the form is valid
//     if (!form.isValid()) {
//       return;
//     };
//     
//     var bookmark = new Kohive.models.Bookmark(form.getForm().getValues());
//     bookmark.data.hive_id = Kohive.OS.getCurrentHive().data.id;
//     
//     bookmark.save({
//       params:  {
//         hiveId: bookmark.data.hive_id //TODO: remove this, server should accept bookmark[hive_id] instead
//       },
//       success: function() {
//         form.el.slideOut('t', { callback: form.hide, scope: form });
//         controller.fireAction('refresh');
//       },
//       failure: function(errors) {
//         controller.handleErrors(errors, form);
//       }
//     });
//     
//     form.afterSubmit();
//   });
//   
//   Ext.applyIf(this, Kohive.ux.AppController);
//   
//   this.initDefault();
// };
// Ext.extend(Kohive.app.BookmarksController, ExtMVC.Controller, {
//   /**
//    * Function description
//    * @param {Object} args Dataview, Bookmark, node
//    */
//   gotoBookmark: function(args) {
//     console.log('gotoBookmark() - Not implemented: Need to fix the double click/single click problem.');
//   }
// });
// 
// Kohive.OS.registerController('bookmarks', Kohive.app.BookmarksController);
// 
// Ext.ns('Kohive.views.bookmarks');

/**
 * Kohive.app.ChatController
 * @extends ExtMVC.Controller
 * Shows the chat window
 */
Kohive.app.ChatController = Ext.extend(Kohive.controllers.KohiveController, {
  name:  'chat',
  title: Ext.ux.Translator.translate('app_chat_room'),
  
  constructor: function() {
    Kohive.app.ChatController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.chat
    });
    
    var os = ExtMVC.OS.getOS();
    var controller = this;

    this.registerViews({
      'index': Kohive.views.chat.Index
    });
    
    /**
     * @action index 
     * Action description
     */
    this.registerAction('index', function() {
      // Render the index window
      this.indexWindow = this.renderView('index', {});
      
      // Show the window
      this.indexWindow.show();
    });
  },
  
  /**
   * @return {String} The channel name for the current hive
   */
  currentHiveChannelName: function() {
    return String.format("#KohiveHive--{0}", Kohive.OS.getCurrentHive().data.hive.irc_channel_id);
  }
});

ExtMVC.OS.getOS().registerController('chat', Kohive.app.ChatController);

Ext.ns('Kohive.views.chat');

/**
 * @class Kohive.app.CommentsController
 * @extends ExtMVC.Controller
 * Manages the finding and creation of comments.
 * This is different to most controllers because many of the methods take a reference to
 * another model - the model to attach the comment to
 */
Kohive.app.CommentsController = Ext.extend(ExtMVC.Controller, {
  constructor: function() {
    Kohive.app.CommentsController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.comments
    });
    
    this.initEvents();
    this.initListeners();
  },
  
  /**
   * @action load
   * Attempts to load the comments for the supplied model instance
   * @param {ExtMVC.Model.Base} model The model to load comments for
   */
  load: function(model) {
    Kohive.models.Comment.findAll({
      url:     this.urlForModel(model),
      scope:   this,
      listeners: {
        scope: this,
        load:  function(comments) {
          this.fireEvent('load', comments, model);
        }
      }
    });
  },
  
  /**
   * @action create
   * Attempts to create a new comment
   * @param {ExtMVC.Model.Base} model The model to attach the new comment to
   * @param {String} commentText The text of the comment to create
   */
  create: function(model, commentText) {
    var comment = new Kohive.models.Comment({content: commentText});
    
		this.fireEvent('before-create', comment, model);

    comment.save({
      url:     this.urlForModel(model),
      scope:   this,
      success: function(record, response) {
        //create the comment from the response
        var comment = new Kohive.models.Comment(Ext.decode(response.responseText));
        
        this.fireEvent('create', comment, model);
      },
      failure: function() {
        this.fireEvent('create-failed', comment, model);
      }
    });
  },
  
  /**
   * @action update
   * Attempts to update an existing comment
   * @param {Kohive.models.Comment} comment The comment to update
   * @param {Object} fields A key : value object of fields to update on this comment
   */
  update: function(comment, fields) {
    for (var key in fields) {
      comment.set(key, fields[key]);
    }
    
    comment.save({
      url:     this.commentUrl(comment),
      scope:   this,
      success: function() {
        this.fireEvent('update', comment);
      }
    });
  },
  
  /**
   * @action destroy
   * Attempts to destroy an existing comment
   * @param {Kohive.models.Comment} comment The comment to destroy
   */
  destroy: function(comment) {
    comment.destroy({
      url:     this.commentUrl(comment),
      scope:   this,
      success: function() {
        this.fireEvent('destroy', comment);
      },
      failure: function() {
        this.fireEvent('destroy-failure', comment);
      }
    });
  },
  
  /**
   * Generates a url to retrieve comments from for a given model instance.
   * This really ought to be done at the model layer :/
   * @param {ExtMVC.Model.Base|Null} model The model to generate the comments url for, or null for Comments index
   * @return {String} The url for comments on the given model
   */
  urlForModel: function(model) {
    if (typeof model == 'undefined') {
      return this.scopedUrl("/comments");
    } else {
      var refId   = model.get('id'),
          refType = model.modelName;
          
      return this.scopedUrl(String.format("/comments?reference_id={0}&reference_type={1}", refId, refType));
    }
  },
  
  /**
   * Returns the scoped url for a given comment instance
   * @param {Kohive.models.Comment} comment The comment to retrieve a url for
   * @return {String} The comment's scoped url
   */
  commentUrl: function(comment) {
    return this.scopedUrl(String.format("/comments/{0}", comment.get('id')));
  },
  
  /**
   * Scopes a given url to the current membership
   * @param {String} url The url fragment to scope to the current membership
   * @return {String} The scoped url
   */
  scopedUrl: function(url) {
    var memId = Kohive.OS.getCurrentHive().get('id');
    
    return String.format("/memberships/{0}{1}", memId, url);
  },
  
  /**
   * Sets up all of the events emitted by this controller
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event load
       * Fires when comments for a particular model have been loaded
       * @param {Ext.data.Store} comments An Ext.data.Store containing the comments for this model
       * @param {ExtMVC.Model.Base} model The model instance that comments were found for
       */
      'load',
      
			/**
			 * @event before-create
			 * Fires when a new comment is going to be created
       * @param {Kohive.models.Comment} comment The comment about to be created
       * @param {ExtMVC.Model.Base} model The model the comment was created on
			 */
			'before-create',

      /**
       * @event create
       * Fires when a new comment has been successfully created
       * @param {Kohive.models.Comment} comment The newly created comment
       * @param {ExtMVC.Model.Base} model The model the comment was created on
       */
      'create',
      
      /**
       * @event update
       * Fires when a comment has been successfully updated
       * @param {Kohive.models.Comment} comment The newly updated comment
       */
      'update',
      
      /**
       * @event destroy
       * Fires when a comment has been successfully destroyed
       * @param {Kohive.models.Comment} comment The comment that was destroyed
       */
      'destroy'
    );
  },
  
  /**
   * Sets up listeners on other controllers
   */
  initListeners: function() {
    Kohive.OS.getController('photos').on({
      scope       : this,
      'show-image': this.load
    });
  }
});

Kohive.OS.registerController('comments', Kohive.app.CommentsController);
Ext.ns('Kohive.views.comments');

/**
 * Kohive.app.DoodleController
 * @extends ExtMVC.Controller
 * Controls the Doodle application
 */
Kohive.app.DoodleController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'doodle',
  
	constructor: function() {
    Kohive.app.DoodleController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.doodle
    });
    
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index', this.index);
  },

  /**
   * @action index 
   * Renders the index view
   */
  index: function() {
		this.renderView('index', {
			closeAction: 'hide',
      autoShow:    true
    }).show();
    
    this.fireEvent('index');
  }
});

ExtMVC.OS.getOS().registerController('doodle', Kohive.app.DoodleController);

Ext.ns('Kohive.views.doodle');

/**
 * @class Kohive.app.FileCabinetController
 * @extends ExtMVC.Controller
 * Manages the File model
 */
Kohive.app.FileCabinetController = Ext.extend(Kohive.controllers.KohiveController, {
  name:  'filecabinet',
	title: Ext.ux.Translator.translate('app_file_cabinet'),
  
  constructor: function() {
    Kohive.app.FileCabinetController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.filecabinet
    });
    
    this.initEvents();
    
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index',   this.index);
    this.registerAction('refresh', this.onRefresh);
  },

  /**
   * @action index 
   * Renders the index view
   */
  index: function() {
    var store = Kohive.models.File.findAll({
      url: this.scopedUrl('file_cabinets'),
			listeners: {
				scope        : this,
				'beforeload' : this.fireEvent.createDelegate(this, ['before-load-files']),
				'load'       : this.fireEvent.createDelegate(this, ['files-loaded'])
			}
    });
    
    this.indexWindow = this.renderView('index', {
      store:       store,
      closeAction: 'hide',
      autoShow:    true,
      listeners: {
        scope           : this,
        'destroy-file'  : this.destroy,
        'download-file' : this.download,
        'edit-file'     : this.edit,
        'update'        : this.update
      }
    });
		
		store.on('load', function(store) {
      this.indexWindow.initTagsStore(store);
    }, this);
		
		this.indexWindow.show();
    
    this.fireEvent('index');
  },
	
	/**
   * Refreshes the the store (if defined) in the index view of the current controller
   */
  onRefresh: function(shortcut) {
    // Get the index view
    var v = this.indexWindow;
		
    // Check if the store exists, and if it does refresh it
    if (v && v.dataGrid && v.dataGrid.store) {
			if (v.editable.editing || v.uploading) return;
			if (shortcut && shortcut.notificationEl) shortcut.notificationEl.hide();
			
			this.fireEvent('refresh', v.dataGrid.store);
			
      v.dataGrid.store.load();
      v.showRefreshing();
    };
  },
  
  /**
   * @action update
   * Updates the given file with the provided arguments
   * @param {Kohive.models.File} file The file to update
   * @param {Object} fields An object containing keys and values to update
   */
  update: function(file, fields, forceRefresh) {
    this.fireEvent('before-update');
		
    for (var key in fields) {
      file.set(key, fields[key]);
    };
		
    //update the tags array - ideally we wouldn't do this here, should be in the model somehow
    file.data.tags = [];
		if (fields.tag_list) {
	    Ext.each(file.get('tag_list').split(','), function(t) {file.data.tags.push({name: t});}, this);
		};
		
    file.save({
      url:     this.scopedUrl(String.format("file_cabinets/{0}", file.get('id'))),
      scope:   this,
      success: function() {
        this.fireEvent('update', file, forceRefresh);
      }
    });
  },
  
  /**
   * @action destroy 
   * Destroys the passed file and fires a file-destroyed event
   * @param {Kohive.models.File} file The file to destroy
   */
  destroy: function(file) {
    file.destroy({
      url:     this.scopedUrl(String.format("file_cabinets/{0}.json", file.get('id'))),
      scope:   this,
      success: function() {
        this.fireEvent('file-destroyed', file);
      }
    });
  },

	/**
   * @action download 
   * Downloads the passed file and fires a file-downloaded event
   * @param {Kohive.models.File} file The file to destroy
   */
  download: function(file) {
    // Get the url of the file
		var url = file.get('url');
		
		// Open the file in the browser
		// TODO is this the best way to do it?
    window.location.href = url;
  },

	/**
   * @action edit 
   * Downloads the passed file and fires a file-editted event
   * @param {Kohive.models.File} file The file to edit
   */
  edit: function(file) {
    //     // Get the url of the file
    // var url = file.get('url');
    // 
    // //show loading
    // this.indexWindow.showLoading(Ext.ux.Translator.translate('app_statusbar_loading'));
    // 
    // var conn = new Ext.data.Connection();
    // conn.request({
    //  url: this.scopedUrl('file_cabinets/' + file.get('id') + '/edit_in_zoho'),
    //  scope: this,
    //  success: function(end) {
    //    this.indexWindow.showSuccess();
    //    
    //    //get the response text
    //    var response = end.responseText;
    //    
    //    //remove unneeded shit and get the url
    //    var url = response.split("<script language='javascript'>")[1].split("</script>")[0].split("window.location.href='")[1].split("';")[0];
    //    
    //    // Open the file in the browser
    //    // TODO is this the best way to do it?
    //    window.open(url);
    //  },
    //  failure: function() {
    //    this.indexWindow.showFailure();
    //  }
    // });
  },
  
  /**
   * TODO: A hacky method to prepend the /memberships/:membership_id/ segment to the front of an AJAX
   * request url.  This really ought to be handled by the model, so factor it out of here when possible
   * @param {String} url The url to append to the memberships scope
   * @return {String} The fully scoped url
   */
  scopedUrl: function(url) {
    return String.format("/memberships/{0}/{1}", Kohive.OS.getCurrentHive().get('id'), url);
  },
  
  /**
   * Sets up all of the events this controller emits
   */
  initEvents: function() {
    this.addEvents(
			/**
			 * @event refresh
			 * Fires when the index window has been refreshed
			 * @param {paramType} paramName parameter description
			 */
			'refresh',
			
      /**
       * @event before-load-files
       * Fires before the request to load (or refresh) files is sent
       */
      'before-load-files',
      
      /**
       * @event files-loaded
       * Fired whenever files have been retrieved
       * @param {Ext.data.Store} files A store containing files
       */
      'files-loaded',
      
      /**
       * @event show-image
       * Fired whenever subscriber views should update the photo they are displaying
       * @param {Kohive.models.Image} image The new image to display
       */
      'show-image',
      
      /**
       * @event file-destroyed
       * Fired when a file has been successfully destroyed
       * @param {Kohive.models.File} file The file that was destroyed
       */
      'file-destroyed',

			/**
       * @event file-downloaded
       * Fired when a file has been successfully downloaded
       * @param {Kohive.models.File} file The file that was downloaded
       */
      'file-downloaded',
      
      /**
       * @event before-update
       * Fired before an image update request is sent
       */
      'before-update',
      
      /**
       * @event update
       * Fired when an image has been successful updated
       * @param {Kohive.models.Image} image The image that was updated
       */
      'update',
      
      /**
       * @event before-create-album
       * Fires before the request to create an album is sent
       */
      'before-create-album',
      
      /**
       * @event album-created
       * Fired when a new album has been successfully created
       * @param {Kohive.models.Album} album The newly created album
       */
      'album-created',
      
      /**
       * @event album-destroyed
       * Fired when an album has been destroyed
       * @param {Kohive.models.Album} album The album that was destroyed
       */
      'album-destroyed'
    );
  }

});

Kohive.OS.registerController('filecabinet', Kohive.app.FileCabinetController);

Ext.ns('Kohive.views.filecabinet');

/**
 * @class Kohive.controllers.IdCardsController
 * @extends Kohive.controllers.KohiveController
 * Manages the Id Card model
 */
Kohive.controllers.IdCardsController = Ext.extend(Kohive.controllers.KohiveController, {
	title: Ext.ux.Translator.translate('app_id_card'),
  
  constructor: function() {
    Kohive.controllers.IdCardsController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.idcards
    });
   
    this.initEvents();
    this.initListeners();
		
		this.on('read', this.edit, this);
		
		//TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
		//TODO: DIRTY HACK to get the id of the id card using os params
    this.registerAction('show', function(idCard) {
			if (idCard && typeof idCard == "object") return this.fireEvent('read', idCard);
			
			var id = Kohive.OS.params.id;
			if (!id) return;
			
			this.read(id);
		});
  },
  
  /**
   * Attempts to load all Id Cards for the current user.
   * Fires the 'index' event if successful
   * @param {Ext.Component} view An optional view component that this controller should listen to
   * @return {Ext.data.Store} The store containing Id Cards.  This won't be loaded yet but may
   * be useful to UI elements which are bound to a store
   */
  index: function(view) {
    if (typeof view != 'undefined' && view != null) {
      view.on({
        scope     : this,
        'use'     : this.use,
        'destroy' : this.destroy,
        'verify'  : this.verify,
        'add'     : this.build,
        'edit'    : this.edit
      });
    }
    
    return Kohive.models.IdCard.findAll({
      scope: this,
			url:   this.scopedUrl("id_cards"),
      listeners: {
        scope: this,
        load:  function(idCards) {
          this.fireEvent('index', idCards);
        },
        loadexception: function() {
          this.fireEvent('index-failed');
        }
      }
    });
  },
  
  /**
   * Displays the New Id Card form and listens to events on it
   */
  build: function() {
    if (typeof this.newIdCardWindow == 'undefined') {
      this.newIdCardWindow = this.renderView('new', {
        closeAction: 'hide',
        autoShow:    true,
        listeners: {
          scope  : this,
          'save' : this.create
        }
      });
    }
    
    this.newIdCardWindow.show();
  },
  
  /**
   * Opens the edit window for a particular Id Card
   * @param {Kohive.models.IdCard} idCard The Id Card to edit
   */
  edit: function(idCard) {
		var key = String.format("show-{0}", idCard.get('id'));
		
    this.editIdCardWindow = this.renderView('show', {
      idCard:   idCard,
			autoShow: true,
      listeners: {
        scope             : this,
        'update'          : this.update,
				'upload-complete' : this.fireEvent.createDelegate(this, ['update'])
      },
      viewCacheKey: key
    });
		
    // this.editIdCardWindow.show();
  },
  
  /**
   * Attempts to create a new Id Card
   * @param {Object} data A fields object (e.g. {title: 'My Id Card'})
   * @param {Ext.form.BasicForm} form The BasicForm from which to take values.  If supplied this
   * will be used instead of the data argument and a fileUpload Ajax request sent
   */
  create: function(data, form) {
		var idCard = new Kohive.models.IdCard(data);
		
    idCard.save({
      scope:    this,
      // isUpload: true,
      form:     typeof form == 'undefined' ? null : form.getEl(),
			url:      this.scopedUrl("id_cards"),
      success: function(idCard, response) {
        this.fireEvent('create', idCard);
      },
      failure: function(idCard, response) {
        this.fireEvent('create-failed', idCard);
      }
    });
  },
  
  /**
   * Attempts to find (read) a single ID card
   * @param {Number} id The Id of the ID Card to read
   */
  read: function(id) {
    Kohive.models.IdCard.findById(id, {
      scope: this,
			url:   this.scopedUrl(String.format("id_cards/{0}", id)),
      success: function(idCard) {
        this.fireEvent('read', idCard);
      },
      failure: function() {
        this.fireEvent('read-failed', id);
      }
    });
  },
  
  /**
   * Attempts to update an existing Id Card with new values
   * @param {Kohive.models.IdCard} idCard The existing Id Card object
   * @param {Object} updates An object containing updates to apply to the Id Card
   */
  update: function(idCard, updates, forceRefresh) {
    for (var key in updates) {
      idCard.set(key, updates[key]);
    };
    
    idCard.save({
      scope:   this,
			url:     this.scopedUrl(String.format("id_cards/{0}", idCard.get('id'))),
      success: function() {
        this.fireEvent('update', idCard, updates, forceRefresh);
      },
      failure: function() {
        this.fireEvent('update-failed', idCard, updates);
      }
    });
  },
  
  /**
   * Attempts to destroy an existing Id Card
   * @param {Kohive.models.IdCard} idCard The Id Card instance to destroy
   */
  destroy: function(idCard) {
		// TODO: for some reason this is being called by the event 'destroy' when closing the settings
		//       app. the only way around it is to check if idCard.get is defined. don't know what
		//       is causing it :/
		if (!idCard || !idCard.get) return;
		
    idCard.destroy({
      scope:   this,
			url:     this.scopedUrl(String.format("id_cards/{0}", idCard.get('id'))),
      success: function() {
        this.fireEvent('destroy', idCard);
      },
      failure: function() {
        this.fireEvent('destroy-failed', idCard);
      }
    });
  },
  
  /**
   * Attempts to mark an Id Card as the one to use in the current hive
   * @param {Kohive.models.IdCard} idCard The Id Card to use
   */
  use: function(idCard) {
    var mem = Kohive.OS.getCurrentHive(); //returns the current membership :/
    mem.set('id_card_id', idCard.get('id'));
    
    mem.save({
      scope:   this,
      success: function() {
        this.fireEvent('hive-id-card-update', idCard);
      },
      failure: function() {
        this.fireEvent('hive-id-card-update-failed', idCard);
      }
    });
  },
	
	/**
	 * Attempts to verify an id cards email address
	 */
	verify: function(idCard) {
    idCard.save({
      scope:   this,
			url:     this.scopedUrl('verify'),
			success: function() {
				this.fireEvent('verify-success');
			},
      failure: function() {
				this.fireEvent('verify-failed');
      }
    });
	},
  
  /**
   * Sets up listeners on a view element.
   */
  initListeners: function() {
    Ext.getCmp('memberOptions').on({
      scope: this,
      'show-idcard': function(id) {
        Kohive.models.IdCard.findById(id, {
          scope:   this,
					url:     this.scopedUrl(String.format("id_cards/{0}", id)),
          success: function(idCard) {
            this.edit(idCard);
          }
        });
      }
    });
  },
  
  /**
   * Sets up events emitted by this 
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event create
       * Fired when a new Id Card has been successfully created
       * @param {Kohive.models.IdCard} idCard The newly created Id Card
       */
      'create',
      
      /**
       * @event create-failed
       * Fired when an attempt to create a new Id Card failed
       * @param {Kohive.models.IdCard} idCard The idCard object which couldn't be saved
       */
      'create-failed',
      
      /**
       * @event read
       * Fired when a single Id Card has been loaded from the database
       * @param {Kohive.models.IdCard} idCard The Id Card instance that was loaded
       */
      'read',
      
      /**
       * @event read-failed
       * Fired when an attempt to load a single Id Card failed
       * @param {Number} id The ID of the Id Card we were attempting to find
       */
      'read-failed',
      
      /**
       * @event update
       * Fired when an existing Id Card has been successfully created
       * @param {Kohive.models.IdCard} idCard The updated Id Card
       * @param {Object} updates The updates object that was supplied
       */
      'update',
      
      /**
       * @event update-failed
       * Fired when an attempty to update an existing Id Card failed
       * @param {Kohive.models.IdCard} idCard The Id Card we were attempting to update
       * @param {Object} updates The object of updates we were trying to apply
       */
      'update-failed',
      
      /**
       * @event destroy
       * Fired when an existing Id Card has been successfully destroyed
       * @param {Kohive.models.IdCard} idCard The Id Card that was destroyed
       */
      'destroy',
      
      /**
       * @event destroy-failed
       * Fired when an attempt to destroy an existing Id Card failed
       * @param {Kohive.models.IdCard} idCard The Id Card we were trying to destroy
       */
      'destroy-failed',
      
      /**
       * @event index
       * Fired when an Id Cards collection has been loaded from the database
       * @param {Ext.data.Store} idCards An Ext.data.Store containing the Id Cards retrieved
       */
      'index',
      
      /**
       * @event index-failed
       * Fired when an error was encountered while trying to load the Id Cards from the database
       */
      'index-failed',
      
      /**
       * @event hive-id-card-update
       * Fired when the Id Card used in the current hive has been successful updated.  This is fired
       * by the 'use' action if the save went through correctly
       * @param {Kohive.models.IdCard} idCard The Id Card now being used for the current Hive
       */
      'hive-id-card-update',
      
      /**
       * @event hive-id-card-update-failed
       * Fired if the user wanted to update the Id Card used in the current Hive but the operation failed
       * @param {Kohive.models.IdCard} idCard The Id Card the user wanted to set
       */
      'hive-id-card-update-failed',
			
			/**
			 * @event verify-success
			 * Fires if verify request is successful
			 */
			'verify-success',
			
			/**
			 * @event verify-failed
			 * Fires if the verify request fails
			 */
			'verify-failed'
    );
  }
});

ExtMVC.OS.getOS().registerController('idcards', Kohive.controllers.IdCardsController);

Ext.ns('Kohive.views.idcards');

/**
 * Kohive.app.InviteController
 * @extends ExtMVC.Controller
 * Manages the invites section of the site
 */
Kohive.app.InviteController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'invite',
	title: Ext.ux.Translator.translate('invite_users'),
  
  constructor: function() {
    Kohive.app.InviteController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.invite
    });
    
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index',   this.index);
    // this.registerAction('refresh', this.onRefresh);
		
		this.initEvents();
  },

	initEvents: function() {
		this.addEvents(
		  /**
		   * @event import-successful
		   * fires whe
		   * n an import was successful
		   */
		  'import-successful',
			
			/**
		   * @event import-failed
		   * fires when an import failed
		   */
		  'import-failed'
		);
	},

  /**
   * @action index 
   * Renders the index view
   */
  index: function(contact) {
		if (!contact) return;
		
		if (this.indexWindow) {
			this.indexWindow.contact = contact;
			this.indexWindow.show();
			return;
		};
		
    this.indexWindow = this.renderView('index', {
			contact: 		 contact,
      closeAction: 'hide',
      autoShow:    true,
			listeners: {
				'import-contacts': {
					scope: this,
					fn:    this.onImport
				}
			}
    });
		
		this.indexWindow.show();
  },
	
	/**
	 * Imports all the records
	 */
	onImport: function(records) {
		//make a string of all the records emails
		var str;
		
		Ext.each(records, function(record) {
			str = (str) ? str + " " + record.get('email') : record.get('email');
		}, this);
    
    // Create a new membership from the above values
    var membership = new Kohive.models.Membership({
			'email':   str,
      'hive_id': Kohive.OS.getCurrentHiveId()
		});
    
		var controller = this;
		
    // Attempt to save the membership to the server
    membership.save({
			scope:   this,
      success: function() {
				controller.indexWindow.hideLoading();
				controller.indexWindow.hide();
      },
      failure: function() {
				controller.indexWindow.showFailure();
      }
    });
	}
});

ExtMVC.OS.getOS().registerController('invite', Kohive.app.InviteController);

Ext.ns('Kohive.views.invite');

/**
 * Kohive.app.MicroBlogController
 * @extends ExtMVC.Controller
 * Manages the addition and removal of bookmarks
 */
Kohive.app.MicroBlogController = Ext.extend(Kohive.controllers.KohiveController, {
  name:  'microblogs',
	title: Ext.ux.Translator.translate('app_micro_blog'),
  
  constructor: function() {
    Kohive.app.PhotosController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.microblogs
    });
    
    this.initEvents();
    		
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index', this.index);
		this.registerAction('refresh', this.onRefresh);
  },
  
  /**
   * Renders the index view
   */
  index: function() {
    this.indexWindow = this.renderView('index', {
      listeners: {
        scope : this,
        create: this.create,
        'entered-twitter-pin': this.onEnteredTwitterPin,
        'turn-off-twitter-sync': this.onTurningOffTwitterSync
      }
    });
  },
  
  /**
   * Attempts to create a new MicroBlog entry
   * @param {Object} data A fields object (e.g. {message: 'I am awesome'})
   * @param {Ext.form.BasicForm} form The BasicForm from which to take values.  If supplied this
   * will be used instead of the data argument and a fileUpload Ajax request sent
   */
  create: function(data, form) {
		var microBlog = new Kohive.models.MicroBlog(data);
		
    microBlog.save({
      scope:    this,
      // isUpload: true,
      form:     typeof form == 'undefined' ? null : form.getEl(),
			url:      this.scopedUrl("microblogs"),
      success: function(idCard) {
        this.fireEvent('create', microBlog);
        this.onRefresh();
      },
      failure: function(idCard) {
        this.fireEvent('create-failed', microBlog);
      }
    });
  },
  
  /**
   * Authorizes the current user with twitter on the backend, using the entered pin
   * @param {String} pin The from the user entered pin
   */
  onEnteredTwitterPin: function (pin) {
    Ext.Ajax.request({
      url: '/twitter/auth/' + pin,
      method: 'POST',
      scope: this,
      params: {
        membership_id: Kohive.OS.getCurrentHive().get('id')
      },
      success: function(response) {
        this.fireEvent('twitter-authorized');
        this.fireAction('refresh');
      }
      
    });
  },
  
  onTurningOffTwitterSync: function () {
    Ext.Ajax.request({
      url: '/twitter/turn_off',
      method: 'POST',
      scope: this,
      params: {
        membership_id: Kohive.OS.getCurrentHive().get('id')
      },
      success: function(response) {
        this.fireEvent('turned-off-twitter-sync');
      }
    });
  },
  
  /**
   * Returns whether the microblogs app is synced with twitter or not
   */
  isSyncedWithTwitter: function() {
    if (this.indexWindow) {
      return this.indexWindow.syncWithTwitter;
    }
  },
	
  onRefresh: function() {
    if (!this.indexWindow || !this.indexWindow.store) return;
    
    var store = this.indexWindow.store;
    store.load();
    this.indexWindow.showRefreshing();
  },
  
  /**
   * Sets up events emitted by this controller
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event create
       * Fired after a new Microblog has been successfully created
       * @param {Kohive.models.MicroBlog} microBlog The newly created microblog
       */
      'create',
      
      /**
       * @event create-failed
       * Fired if a create attempt failed
       * @param {Kohive.models.MicroBlog} microBlog The microblog that could not be created
       */
      'create-failed',
      
      /**
       * @event entered-twitter-pin
       * Fired after user entered pin
       * @param {String} pin The entered pin
       */
      'entered-twitter-pin'
    );
  }
});

ExtMVC.OS.getOS().registerController('microblogs', Kohive.app.MicroBlogController);

Ext.ns('Kohive.views.microblogs');

// /**
//  * Kohive.app.MicroBlogController
//  * @extends ExtMVC.Controller
//  * Manages the addition and removal of microblog
//  */
// Kohive.app.MicroBlogController = function() {
//   Kohive.app.MicroBlogController.superclass.constructor.call(this, {
//     viewsPackage: Kohive.app.MicroBlog.views
//   });
//   
//   var os = ExtMVC.OS.getOS();
//   var controller = this;
//   
//   this.registerViews({
//     'index': Kohive.app.MicroBlog.views.Index
//   });
//   
//   /**
//    * @action index 
//    * Creates the MicroBlog window, listens to deleteMicroBlog event.
//    */
//   this.registerAction('index', function(config) {
//     var config = config || {};
//     
//     this.store = Kohive.models.MicroBlog.findAll({
//       url: '/responses/microblogs.xml',
//       listeners: {
//         'load': {
//           scope: this,
//           fn: function(store, records) {
//             this.indexWindow.cachedRecords = records;
//           }
//         }
//       }
//     });
//     
//     this.indexWindow = this.renderView('index', Ext.applyIf(config, {
//       store: this.store,
//       listeners: {
//         'create': {
//           scope: this,
//           fn:    function() { this.fireAction('create', this, arguments); }
//         },
//         'update': {
//           scope: this,
//           fn:    function(){ this.fireAction('update', this, arguments); }
//         },
//         'refresh':   this.fireAction.createDelegate(this, ['refresh']),
//         'toggleall': this.fireAction.createDelegate(this, ['toggleall'])
//       }
//     }));
//     
//     this.indexWindow.show();
//     
//     this.indexWindow.on('action-delete', controller.destroy, controller);
//     
//     /**
//      * Define events for 
//      */
//     this.indexWindow.on('action-reply', controller.replyMicroblog,   controller);
//     
//     /**
//      * Define the editable fields
//      */
//     this.indexWindow.on('action-edit-owner_alias', controller.delegateFieldEdit, controller);
//     this.indexWindow.on('action-edit-message',     controller.delegateFieldEdit, controller);
//   });
//   
//   /**
//    * @action create 
//    * Creates a new Bookmark object, saves it
//    */
//   this.registerAction('create', function(form) {
//     // Check if the form is valid
//     if (!form.isValid()) {
//       return;
//     };
//     
//     var microblog = new Kohive.models.MicroBlog(form.getForm().getValues());
//     microblog.data.hive_id = Kohive.OS.getCurrentHive().data.id;
//     
//     microblog.save({
//       params:  {
//         hiveId: microblog.data.hive_id //TODO: remove this, server should accept microblog[hive_id] instead
//       },
//       success: function() {
//         form.el.slideOut('t', { callback: form.hide, scope: form });
//         controller.fireAction('refresh');
//       },
//       failure: function(errors) {
//         controller.handleErrors(errors, form);
//       }
//     });
//     
//     form.afterSubmit();
//   });
//   
//   Ext.applyIf(this, Kohive.ux.AppController);
//   
//   this.initDefault();
// };
// Ext.extend(Kohive.app.MicroBlogController, ExtMVC.Controller, {
//   /**
//    * Function description
//    * @param {Object} args Dataview, model, node
//    */
//   replyMicroblog: function(args) {
//     console.log('replyMicroblog() - Not implemented: Need to fix the double click/single click problem.');
//   }
// });
// 
// Kohive.OS.registerController('microblog', Kohive.app.MicroBlogController);
// 
// Ext.ns('Kohive.views.microblog');

/**
 * @class Kohive.app.OverviewController
 * @extends ExtMVC.Controller
 * Manages the Overview model
 */
Kohive.app.OverviewController = Ext.extend(Kohive.controllers.KohiveController, {
  name:  'overview',
	title: Ext.ux.Translator.translate('app_overview'),
  
  constructor: function() {
    Kohive.app.OverviewController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.overview
    });
    
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index', this.index);
  },

  /**
   * @action index 
   * Renders the index view
   */
  index: function() {
		var store = Kohive.models.Overview.findAll({
      url:      Kohive.OS.scopedUrl('overview'),
			autoLoad: false,
			listeners:  {
        'load': {
          scope: this,
          fn: function(store, records) {
            this.indexWindow.hideLoading();
          }
        }
      }
    });
		
    this.indexWindow = this.renderView('index', {
      store:       store,
      closeAction: 'hide',
      autoShow:    true
    });
		
		this.indexWindow.show();
    
    this.fireEvent('index');
  }
});

Kohive.OS.registerController('overview', Kohive.app.OverviewController);

Ext.ns('Kohive.views.overview');

/**
 * @class Kohive.app.PhotosController
 * @extends ExtMVC.Controller
 * Manages the Album and Image models, renders the Photos main view and emits events when
 * Album and Image changes are made
 */
Kohive.app.PhotosController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'photos',
  
	currentAlbum: {},
	
	originalStore: [],
	
  constructor: function() {
    Kohive.app.PhotosController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.photos
    });
    
    this.initEvents();
		this.initListeners();
    
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index', this.index);
  },

  /**
   * @action index 
   * Renders the index view
   */
  index: function() {
    this.indexWindow = this.renderView('index', {
      albumStore: Kohive.models.Album.findAll({
        url: this.scopedUrl('albums')
      }),
      closeAction: 'hide',
      autoShow:    true,
      listeners: {
        scope          : this,
        'load-album'   : this.loadAlbumImages,
        'refresh'      : this.loadAlbumImages.createDelegate(this, [null, null, true]),
        'filter'       : this.filter,
        'show-image'   : this.show,
        'update-image' : this.update,
        'destroy-image': this.destroy,
        'destroy-album': this.destroyAlbum,
        'create-album' : this.createAlbum,
        'create-rss-album' : this.createRssAlbum
      }
    });
		
		this.indexWindow.show();
    
    this.fireEvent('index');
  },
  
  /**
   * @action update
   * Updates the given image with the provided arguments
   * @param {Kohive.models.Image} image The image to update
   * @param {Object} fields An object containing keys and values to update
   */
  update: function(image, fields) {
    this.fireEvent('before-update');
		
    for (var key in fields) {
      image.set(key, fields[key]);
    };

    //update the tags array - ideally we wouldn't do this here, should be in the model somehow
    image.data.tags = [];
		if (fields.tag_list) {
	    Ext.each(image.get('tag_list').split(' '), function(t) {image.data.tags.push({name: t});}, this);
		};
		
		//update the description_html if description is set
		if (fields.description) {
	    image.data.description_html = fields.description.replace("\n", "<br />");
		};
		
    image.save({
      url:     this.scopedUrl(String.format("albums/{0}/images/{1}", image.get('album_id'), image.get('id'))),
      scope:   this,
      success: function() {
        this.fireEvent('update', image);
      }
    });
  },
  
  /**
   * @action destroy 
   * Destroys the passed photo and fires a image-destroyed event
   * @param {Kohive.models.Image} image The image to destroy
   */
  destroy: function(image) {
    image.destroy({
      url:     this.scopedUrl(String.format("albums/{0}/images/{1}.json", image.get('album_id'), image.get('id'))),
      scope:   this,
      success: function() {
        this.loadAlbumImages();
        this.fireEvent('image-destroyed', image);
      }
    });
  },
  
  /**
   * Filters the currently loaded images with options from a filter bar
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  filter: function(filters) {
		var hive = Kohive.OS.getCurrentHive();
		
    //for some reason passing the params option to findAll isn't working, so build url manually :/
    var url        = this.scopedUrl('albums/' + this.currentAlbum[Kohive.OS.getCurrentHiveId()].get('id') + '/images?'),
        tags       = filters.tags,
        authors    = filters.authors,
        conditions = [];
    
    if (tags && tags.length > 0)       conditions.push(String.format("tags={0}", tags.join(',')));
    if (authors && authors.length > 0) conditions.push(String.format("membership_ids={0}", authors.join(',')));
    
    this.loadAlbumImages(this.currentAlbum[Kohive.OS.getCurrentHiveId()], url + conditions.join("&"));
  },
  
  /**
   * @action show 
   * Fires an event telling subscriber views to update to the given photo
   * @param {Kohive.models.Image} image The image to show
   */
  show: function(image) {
    this.fireEvent('show-image', image);
  },
  
	/**
   * @action createFromUrl
   * 
   * @param {String} url the Url of the album to create
   */
  createFromUrl: function(url) {
    this.fireEvent('before-create-image', url);
    
		var hive = Kohive.OS.getCurrentHive();
		
    new Kohive.models.Image({author_membership_id: hive.get('id'), file_url: url}).save({
      url:   this.scopedUrl('albums/' + this.currentAlbum[Kohive.OS.getCurrentHiveId()].get('id') + '/images'),
      scope: this,
      success: function(album) {
        this.fireEvent('image-created', url);
      },
			failure: function() {
				this.fireEvent('create-image-failed', url);
			}
    });
  },

	/**
   * @action createFromFlickr
   * @param {String} url the Url of the flickr album to import
   */
  createFromFlickr: function(url, form) {
    this.fireEvent('before-create-image', url);
    
		var hive = Kohive.OS.getCurrentHive();
		
    var model = new Kohive.models.Album({});
		model.newRecord = true;
		
		model.save({
      url:   this.scopedUrl('albums'),
      scope: this,
			params: {
				"id": this.currentAlbum[Kohive.OS.getCurrentHiveId()].get('id'),
				"album[title]": url
			},
      success: function(album) {
        this.fireEvent('image-created', url);
				form.reset();
      },
			failure: function() {
				this.fireEvent('create-image-failed', url);
			}
    });
  },
  
  /**
   * @action createAlbum
   * Creates a new album with the given title argument
   * @param {String} title The title of the new album to create
   */
  createAlbum: function(title) {
    this.fireEvent('before-create-album', title);
    
    new Kohive.models.Album({author_membership_id: Kohive.OS.getCurrentHive().get('id'), title: title}).save({
      url:   this.scopedUrl('albums'),
      scope: this,
      success: function(album) {
        this.fireEvent('album-created', album);
      }
    });
  },
  
  /**
   * @action createRssAlbum
   * Creates a new rss album with the given feed_url argument
   * @param {String} feed_url The feed url to fetch the images from
   */
  createRssAlbum: function(feed_url) {
    this.fireEvent('before-create-album', feed_url);
    
    new Kohive.models.Album({
        author_membership_id: Kohive.OS.getCurrentHive().get('id'),
        feed_url: feed_url,
        type: 'FeedPhotoAlbum'
      }).save({
      url:   this.scopedUrl('albums'),
      scope: this,
      success: function(album) {
        this.fireEvent('rss-album-created', album);
      }
    });
  },
  
  /**
   * @action destroyAlbum
   * Attempts to destroy an album, fires an album-destroyed event if successful
   * @param {Kohive.models.Album} album The album to destroy
   */
  destroyAlbum: function(album) {
		this.fireEvent('before-destroy-album', album);
		
		album.destroy({
      url:     this.scopedUrl(String.format("albums/{0}", album.get('id'))),
      scope:   this,
      success: function() {
        this.fireEvent('album-destroyed', album);
      }
    });
  },
  
  /**
   * @action loadAlbumImages
   * Loads images for a specific album and fires the images-loaded event
   * @param {Kohive.models.Album} album The album to load images from
   * @param {String} url An optional url to use (used by the filter action)
   * @param {Boolean} newImage True if it is a new image
   */
  loadAlbumImages: function(album, url, newImage) {
		var hive = Kohive.OS.getCurrentHive();
		if (!album && !this.currentAlbum[Kohive.OS.getCurrentHiveId()]) return;
		if (!album && this.currentAlbum[Kohive.OS.getCurrentHiveId()]) album = this.currentAlbum[Kohive.OS.getCurrentHiveId()];
    this.currentAlbum[Kohive.OS.getCurrentHiveId()] = album;
    
    this.fireEvent('before-load-images', album);
		
    // TODO when creating a new album, it only has a 'title' field, no id. so this must be fixed
		// by returning the id in the response from the server when creating a new album.
    var url = url || this.scopedUrl('albums/' + album.data.id + '/images');
		
    album.images.findAll({
      url: url,
			sortInfo: {
				field:     'created_at',
				direction: 'DESC'
			},
      listeners: {
        scope: this,
        load:  function(store) {
					if (store.proxy.conn.url.split('?').length == 1) this.originalStore = store.data.items;
					this.currentStore = store;
					
					this.fireEvent('images-loaded', store, album);
					
					var image = store.getAt(0);
					this.fireEvent('show-image', image, newImage);
        }
      }
    });
  },
  
  /**
   * TODO: A hacky method to prepend the /memberships/:membership_id/ segment to the front of an AJAX
   * request url.  This really ought to be handled by the model, so factor it out of here when possible
   * @param {String} url The url to append to the memberships scope
   * @return {String} The fully scoped url
   */
  scopedUrl: function(url) {
    return String.format("/memberships/{0}/{1}", Kohive.OS.getCurrentHive().get('id'), url);
  },
  
  /**
   * Sets up all of the events this controller emits
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event before-load-images
       * Fires before the request to load (or refresh) images from an album is sent
       */
      'before-load-images',
      
      /**
       * @event images-loaded
       * Fired whenever images for a specific album have been retrieved
       * @param {Ext.data.Store} images A store containing images
       * @param {Kohive.models.Album} album The album that images were loaded for
       */
      'images-loaded',
      
      /**
       * @event show-image
       * Fired whenever subscriber views should update the photo they are displaying
       * @param {Kohive.models.Image} image The new image to display
       */
      'show-image',
      
      /**
       * @event image-destroyed
       * Fired when a photo has been successfully destroyed
       * @param {Kohive.models.Image} image The image that was destroyed
       */
      'image-destroyed',
      
			/**
			 * @event image-created
			 * Fires when a image has been created using a URL
			 */
			'image-created',
			
			/**
			 * @event before-create-image
			 * Fires before an image is created from a Url
			 */
			'before-create-image',
			
			/**
			 * @event create-image-failed
			 * Fires when creating an image failed
			 */
			'create-image-failed',
			
      /**
       * @event before-update
       * Fired before an image update request is sent
       */
      'before-update',
      
      /**
       * @event update
       * Fired when an image has been successful updated
       * @param {Kohive.models.Image} image The image that was updated
       */
      'update',
      
      /**
       * @event before-create-album
       * Fires before the request to create an album is sent
       */
      'before-create-album',
      
      /**
       * @event album-created
       * Fired when a new album has been successfully created
       * @param {Kohive.models.Album} album The newly created album
       */
      'album-created',
      
      /**
       * @event rss-album-created
       * Fired when a new rss album has been successfully created
       * @param {Kohive.models.Album} album The newly created album
       */
      'rss-album-created',
      
      /**
       * @event album-destroyed
       * Fired when an album has been destroyed
       * @param {Kohive.models.Album} album The album that was destroyed
       */
      'album-destroyed'
    );
  },
	
	/**
	 * Add listeners to the controller
	 */
	initListeners: function() {
		this.on('image-created', function() {
			this.loadAlbumImages(null, null, true);
		}, this);
	}

});

Kohive.OS.registerController('photos', Kohive.app.PhotosController);

Ext.ns('Kohive.views.photos');

/**
 * Kohive.app.SettingsController
 * @extends ExtMVC.Controller
 * Controls the Settings application
 */
Kohive.app.SettingsController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'settings',
  
	constructor: function() {
    Kohive.app.SettingsController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.settings
    });
    
		this.initListeners();
		
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index', this.index);
  },
	
	initListeners: function() {
		var controller = Kohive.OS.getController('idcards');
		
		controller.on({
			scope            : this,
			'verify-success' : this.onVerifySuccess,
			'verify-failed'  : this.onVerifyFailure
		});
	},
	
  /**
   * @action index 
   * Renders the index view
   */
  index: function(pane) {
		this.indexWindow = this.renderView('index', {
			closeAction: 'hide',
      autoShow:    true,
      listeners: {
        'changecard': {
          fn: function(panel, oldId, newId) {
            //update window title
            try {
              panel.setTitle(Ext.ux.Translator.translate('app_settings') + " : " + panel.mainPanel.layout.activeItem.title);
            } catch(e) {};
          }
        },
        'render': {
          scope: this,
          fn:    this.attachChildViewEvents
        }
      }
    });
		
		this.indexWindow.show();
		
		//check if a pane has been defined, and if so, try and activate it
		if (pane) {
			//check if the user needs permission
			if ((pane == "settings_view_hive_applications" || pane == "settings_view_hive_appearance") && !Kohive.OS.hasPermission(true)) return;
			
			//add the hiveid onto the end
			var pane = pane + Kohive.OS.getCurrentHiveId();
			
			//update the menu's active item
			this.indexWindow.menuPanel.setActiveItem(pane);
			
			//set the active item
			this.indexWindow.mainPanel.layout.setActiveItem(pane);
			
			//update window title
      try {
        this.indexWindow.setTitle("Settings : " + this.indexWindow.mainPanel.layout.activeItem.title);
      } catch(e) {};
			
			//fire off the activate event
			var item = this.indexWindow.mainPanel.layout.container.getComponent(pane);
	    if (item) {
	      item.fireEvent('activate', item);
	    };
		};
    
    this.fireEvent('index');
  },
  
  /**
   * Adds the various listeners to child panels of the settings window
   * Intended to be attached to the Window's render event
   * @param {Ext.Window} indexWindow The main settings window
   */
  attachChildViewEvents: function(indexWindow) {
    this.indexWindow = this.indexWindow || indexWindow;
		
		this.attachChildEvent('settings_view_hive_id_card' + Kohive.OS.getCurrentHiveId(), {
      'refresh': {
        fn: function() {
          this.indexWindow.showRefreshing();
        }
      },
			'destroy': {
        fn: function() {
          this.indexWindow.showDestroying();
        }
      },
			'verify': {
        fn: function() {
          this.indexWindow.showSubmitting();
        }
      },
			'refreshed': {
				fn: function() {
					this.indexWindow.hideLoading();
				}
			}
    });
  },
  
  /**
   * Attaches events for a single sub view (private)
   * @param {Ext.Window} indexWindow The main settings window
   */
  attachChildEvent: function(viewId, listeners) {
    //add a default scope of *this* for each listener
    for (l in listeners) {
      Ext.applyIf(listeners[l], {
        scope: this
      });
    };
    
    // Add each of the listeners to the specified view
    try {
      for (l in listeners) {
        this.indexWindow.mainPanel.findById(viewId).on(l, listeners[l].fn, this);
      };
    } catch(e) {};
  },
	
	onVerifySuccess: function() {
		if (this.indexWindow) this.indexWindow.showSuccess();
	},
	
	onVerifyFailure: function() {
		if (this.indexWindow) this.indexWindow.showFailure();
	}
});

Kohive.OS.registerController('settings', Kohive.app.SettingsController);

Ext.ns('Kohive.views.settings.cards');

/**
 * Kohive.app.StickiesController
 * @extends ExtMVC.Controller
 * Manages the addition and removal of stickies
 */
Kohive.app.StickiesController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'stickies',
  
  constructor: function(config) {
    var config = config || {};

    // Get the OS and the controller
    var os         = ExtMVC.OS.getOS();
    var controller = this;

    Kohive.app.StickiesController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.stickies
    });

    this.actsAsKohiveController(Kohive.models.Sticky);
    
    /**
     * @action destroy 
     * Deletes a given model instance
     */
    this.registerAction('destroy', function(record, store, contentEl) {
      if (record && store && contentEl) {
        var u = new this.model({id: id});
        u.destroy({
          url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/' + this.model.modelName.pluralize() + '/' + record.get('id'),
          scope:   this,
          success: function() {
            this.onDestroySuccess(store, contentEl, record);
            Kohive.OS.desktopStickiesDataview.store.reload();
          },
          failure: this.onDestroyFailure
        });
      };
    });
  }
});

ExtMVC.OS.getOS().registerController('stickies', Kohive.app.StickiesController);

Ext.ns('Kohive.views.stickies');

/**
 * Kohive.app.TasksController
 * @extends ExtMVC.Controller
 * Manages the addition and removal of tasks
 */
Kohive.app.TasksController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'tasks',
  
	/**
	 * 
	 */
	files: {},
	
	/**
	 * 
	 */
	milestones: {},
	
  constructor: function(config) {
    var config = config || {};

    // Get the OS and the controller
    var os         = ExtMVC.OS.getOS();
    var controller = this;

    Kohive.app.TasksController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.tasks
    });

    this.actsAsKohiveController(Kohive.models.Task);
		
		/**
     * @action create 
     * Creates a new model instance, displays errors if required and redirects to index
     */
    this.registerAction('create', function(form, date) {
      // form.el.mask('Saving...', 'x-mask-loading');
      this.onCreate(form, date);
    });
		
		Ext.apply(this, {
		  /**
		   * Called when the save button on an edit form is pressed.  By default this will load the model object with the form values,
		   * then call save() on the model object.  Override onCreateSuccess and onCreateFailure to update success and failure callbacks
		   * @param {Ext.form.BasicForm} form A reference to the FormPanel
		   */
		  onCreate: function(form, date) {
		    this.newModelObj = new this.model({});

		    this.form = form;
				var date = (date.format) ? date.format('j M y') : date;
				
		    this.onSave(form, this.newModelObj, {
		      success:  this.onCreateSuccess,
		      failure:  this.onCreateFailure
		    }, date);
		  },
		
			/**
	     * Used by both onCreate and onUpdate.  Don't override this unless you understand what you're doing
	     */
	    onSave: function(form, model, options, date) {
	      var v = this.indexWindow;
	      v.showSubmitting();

	      // Get the values from the form
	      var values = form.getValues();
				values.due = date;
				values.task_type = values.type;
				
				//change the array into a string
        if (values.tag_list && Ext.isArray(values.tag_list)) values.tag_list = values.tag_list.join(' ');
        
	      // Get the current membership id
	      values.author_membership_id = Kohive.OS.getCurrentHive().get('id');

	      /**
	       * Updates the model (which also saves it).  Uses success and failure options passed in from
	       * one of onUpdate or onCreate, depending on which one called this
	       */
	      model.update(values, Ext.apply({}, options, {
	        scope:    this,
	        url:      this.saveUrl,
	        callback: function() {form.el.unmask();}
	      }));
	    },
			
			/**
		   * Called after successful item creation.  By default this just first onSaveSuccess
		   */
		  onCreateSuccess: function() {
			  this.onSaveSuccess();
				
				var tempTask = new Ext.util.DelayedTask(function() {
				  var field = this.form.items.items[2];
  				field.focus();
				}, this);
				tempTask.delay(1000);
			}
		});
  },
  
  getMilestones: function() {
    if (this.milestones[Kohive.OS.getCurrentHiveId()]) {
      this.fireEvent('milestones', this.milestones[Kohive.OS.getCurrentHiveId()]);
      return;
    };
    
    //get all the files
		var store = Kohive.models.Milestone.findAll({
      url: Kohive.OS.scopedUrl('milestones'),
			listeners: {
				scope : this,
				'load': function(store) {
					//create a array with the default 'none' value
					var milestones = [[null, Ext.ux.Translator.translate('none'), null]];
          
					//loop through each of the store items
					store.each(function(record) {
						milestones.push([record.get('id'), record.get('title'), record.get('due_at')]);
					}, this);
					
					//create and return the store
					var milestonesStore = new Ext.data.SimpleStore({
			      fields: ['id', 'title', 'due_at'],
			      data:   milestones
			    });
					
					this.milestones[Kohive.OS.getCurrentHiveId()] = milestonesStore;
					
					this.fireEvent('milestones', this.milestones[Kohive.OS.getCurrentHiveId()]);
				}
			}
		});
  }
});

ExtMVC.OS.getOS().registerController('tasks', Kohive.app.TasksController);

Ext.ns('Kohive.views.tasks');

/**
 * @class Kohive.app.UsersController
 * @extends ExtMVC.Controller
 * Manages the kohive Users
 */
Kohive.app.UsersController = Ext.extend(Kohive.controllers.KohiveController, {
  name:  'users',
  
  constructor: function() {
    Kohive.app.UsersController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.users
    });
    
    this.initEvents();
  },
  
  /**
   * @action update
   * Updates the given user with the provided arguments
   * @param {Kohive.models.User} user The user instance which needs to be updated
   * @param {Object} fields The fields to be updated on the user
   */
  update: function(user, fields, opts) {
    this.fireEvent('before-update');
		
    for (var key in fields) {
      user.set(key, fields[key]);
    };
		
    user.save(Ext.applyIf(opts || {}, {
      url:     '/update_user',
      scope:   this,
      success: function() {
        this.fireEvent('update', user);
      },
			failure: function(model) {
				this.fireEvent('update-failed', user, model);
			}
    }));
  },
  
  /**
   * @action destroy 
   * Destroys the passed user and fires a user-destroyed event
   * @param {Kohive.models.User} user The user to destroy
   */
  destroy: function(user) {
		user.destroy({
			url: '/leave',
			success: function() {
				return Ext.Msg.show({
		      title:   '',
		      msg:     Ext.ux.Translator.translate('destroy_account_goodbye_desc')
		    });
			},
			failure: function() {
				return Ext.Msg.show({
		      title:   '',
		      msg:     Ext.ux.Translator.translate('destroy_account_error_desc'),
					buttons: {no: Ext.ux.Translator.translate('cancel')}
		    });
			}
		});
  },
	
  /**
   * TODO: A hacky method to prepend the /memberships/:membership_id/ segment to the front of an AJAX
   * request url.  This really ought to be handled by the model, so factor it out of here when possible
   * @param {String} url The url to append to the memberships scope
   * @return {String} The fully scoped url
   */
  scopedUrl: function(url) {
    return String.format("/memberships/{0}/{1}", Kohive.OS.getCurrentHive().get('id'), url);
  },
  
  /**
   * Sets up all of the events this controller emits
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event before-update
       * Fired before an user update request is sent
       */
      'before-update',
      
      /**
       * @event update
       * Fired when an user has been successful updated
       * @param {Kohive.models.User} user The user that was updated
       */
      'update',
			
			/**
			 * @event update-failed
			 * Fired when the update fails
			 */
			'update-failed',
			
			/**
			 * @event user-destroyed
			 * Fires when a user has been destroyed
			 */
			'user-destroyed'
    );
  }

});

Kohive.OS.registerController('users', Kohive.app.UsersController);

Ext.ns('Kohive.views.users');

/**
 * @class Kohive.app.VideosController
 * @extends ExtMVC.Controller
 * Manages the VideoAlbum and Video models, renders the Videos main view and emits events when
 * VideoAlbum and Video changes are made
 */
Kohive.app.VideosController = Ext.extend(Kohive.controllers.KohiveController, {
  name: 'videos',
	
	currentAlbum: {},
  
  constructor: function() {
    Kohive.app.VideosController.superclass.constructor.call(this, {
      viewsPackage: Kohive.views.videos
    });
    
    this.initEvents();
		this.initListeners();
    
    //TODO: This is a relic of old MVC, remove it once on ExtMVC 0.6 or above
    this.registerAction('index',  this.index);
    this.registerAction('create', this.create);
  },

  /**
   * @action index 
   * Renders the index view
   */
  index: function() {
    this.indexWindow = this.renderView('index', {
      albumStore: Kohive.models.VideoAlbum.findAll({
        url: this.scopedUrl('video_albums')
      }),
      closeAction: 'hide',
      autoShow:    true,
      listeners: {
        scope          : this,
        'load-album'   : this.loadAlbumVideos,
        'refresh'      : this.loadAlbumVideos.createDelegate(this, [null, null, true]),
        'filter'       : this.filter,
        'show-video'   : this.show,
        'update-video' : this.update,
        'destroy-video': this.destroy,
        'destroy-album': this.destroyAlbum,
        'create-album' : this.createAlbum
        // 'create-rss-album' : this.createRssAlbum
      }
    });
		
		this.indexWindow.show();
    
    this.fireEvent('index');
  },
  
  /**
   * @action update
   * Updates the given video with the provided arguments
   * @param {Kohive.models.Video} video The video to update
   * @param {Object} fields An object containing keys and values to update
   */
  update: function(video, fields) {
    this.fireEvent('before-update');
		
    for (var key in fields) {
      video.set(key, fields[key]);
    };

    //update the tags array - ideally we wouldn't do this here, should be in the model somehow
    video.data.tags = [];
		if (fields.tag_list) {
	    Ext.each(video.get('tag_list').split(' '), function(t) {video.data.tags.push({name: t});}, this);
		};
		
		//update the description_html if description is set
		if (fields.description) {
	    video.data.description_html = fields.description.replace("\n", "<br />");
		};
		
    video.save({
      url:     this.scopedUrl(String.format("video_albums/{0}/videos/{1}", video.get('video_album_id'), video.get('id'))),
      scope:   this,
      success: function() {
        this.fireEvent('update', video);
      }
    });
  },

	/**
   * @action create
   * Creates a new video from the form specified
   */
  create: function(form) {
    this.fireEvent('before-update');
		
		this.indexWindow.showSubmitting();
		
   	var video = new Kohive.models.Video(form.getValues());
		
    video.save({
      url:     this.scopedUrl(String.format("video_albums/{0}/videos", this.currentAlbum[Kohive.OS.getCurrentHiveId()].get('id'))),
      scope:   this,
      success: function() {
				this.indexWindow.closeNewVideoView();
				this.loadAlbumVideos(null, null, true);
        this.fireEvent('update', video);
        this.fireEvent('video-created', video);
      },
			failure: function() {
				this.indexWindow.showFailure();
			}
    });
  },
  
  /**
   * @action destroy 
   * Destroys the passed video and fires a video-destroyed event
   * @param {Kohive.models.Video} video The video to destroy
   */
  destroy: function(video) {
    video.destroy({
      url:     this.scopedUrl(String.format("video_albums/{0}/videos/{1}.json", video.get('video_album_id'), video.get('id'))),
      scope:   this,
      success: function() {
        this.loadAlbumVideos();
        this.fireEvent('video-destroyed', video);
      }
    });
  },
  
  /**
   * Filters the currently loaded videos with options from a filter bar
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  filter: function(filters) {
    //for some reason passing the params option to findAll isn't working, so build url manually :/
    var url        = this.scopedUrl('video_albums/' + this.currentAlbum[Kohive.OS.getCurrentHiveId()].get('id') + '/videos?'),
        tags       = filters.tags,
        authors    = filters.authors,
        conditions = [];
    
    if (tags && tags.length > 0)       conditions.push(String.format("tags={0}", tags.join(',')));
    if (authors && authors.length > 0) conditions.push(String.format("membership_ids={0}", authors.join(',')));
    
    this.loadAlbumVideos(this.currentAlbum[Kohive.OS.getCurrentHiveId()], url + conditions.join("&"));
  },
  
  /**
   * @action show 
   * Fires an event telling subscriber views to update to the given video
   * @param {Kohive.models.Video} video The video to show
   */
  show: function(video) {
    this.fireEvent('show-video', video);
  },
  
  /**
   * @action createAlbum
   * Creates a new album with the given title argument
   * @param {String} title The title of the new album to create
   */
  createAlbum: function(title) {
    this.fireEvent('before-create-album', title);
		
    new Kohive.models.VideoAlbum({author_membership_id: Kohive.OS.getCurrentHive().get('id'), title: title}).save({
      url:   this.scopedUrl('video_albums'),
      scope: this,
      success: function(album) {
        this.fireEvent('album-created', album);
      }
    });
  },
  
  // /**
  //  * @action createRssAlbum
  //  * Creates a new rss album with the given feed_url argument
  //  * @param {String} feed_url The feed url to fetch the videos from
  //  */
  // createRssAlbum: function(feed_url) {
  //   this.fireEvent('before-create-album', feed_url);
  //   
  //   new Kohive.models.Album({
  //       author_membership_id: Kohive.OS.getCurrentHive().get('id'),
  //       feed_url: feed_url,
  //       type: 'FeedPhotoAlbum'
  //     }).save({
  //     url:   this.scopedUrl('albums'),
  //     scope: this,
  //     success: function(album) {
  //       this.fireEvent('rss-album-created', album);
  //     }
  //   });
  // },
  
  /**
   * @action destroyAlbum
   * Attempts to destroy an album, fires an album-destroyed event if successful
   * @param {Kohive.models.Album} album The album to destroy
   */
  destroyAlbum: function(album) {
		this.fireEvent('before-destroy-album', album);
		
		album.destroy({
      url:     this.scopedUrl(String.format("video_albums/{0}", album.get('id'))),
      scope:   this,
      success: function() {
        this.fireEvent('album-destroyed', album);
      }
    });
  },
  
  /**
   * @action loadAlbumVideos
   * Loads videos for a specific album and fires the videos-loaded event
   * @param {Kohive.models.Album} album The album to load videos from
   * @param {String} url An optional url to use (used by the filter action)
   * @param {Boolean} newVideo True if it is a new video
   */
  loadAlbumVideos: function(album, url, newVideo) {
    var hive = Kohive.OS.getCurrentHive();
		if (!album && !this.currentAlbum[Kohive.OS.getCurrentHiveId()]) return;
		if (!album && this.currentAlbum[Kohive.OS.getCurrentHiveId()]) album = this.currentAlbum[Kohive.OS.getCurrentHiveId()];
    this.currentAlbum[Kohive.OS.getCurrentHiveId()] = album;
    
    this.fireEvent('before-load-videos', album);
    
    // TODO when creating a new album, it only has a 'title' field, no id. so this must be fixed
		// by returning the id in the response from the server when creating a new album.
    var url = url || this.scopedUrl('video_albums/' + album.get('id') + '/videos');
		
    album.videos.findAll({
      url: url,
			sortInfo: {
				field:     'created_at',
				direction: 'DESC'
			},
      listeners: {
        scope: this,
        load:  function(store) {
					if (store.proxy.conn.url.split('?').length == 1) this.originalStore = store.data.items;
					this.currentStore = store;
					
          this.fireEvent('videos-loaded', store, album);
					
					var video;
					if (newVideo === true) video = store.getAt(0);
          else video = store.getAt(0);
					
					this.fireEvent('show-video', video, newVideo);
        }
      }
    });
  },
  
  /**
   * TODO: A hacky method to prepend the /memberships/:membership_id/ segment to the front of an AJAX
   * request url.  This really ought to be handled by the model, so factor it out of here when possible
   * @param {String} url The url to append to the memberships scope
   * @return {String} The fully scoped url
   */
  scopedUrl: function(url) {
    return String.format("/memberships/{0}/{1}", Kohive.OS.getCurrentHive().get('id'), url);
  },
  
  /**
   * Sets up all of the events this controller emits
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event before-load-videos
       * Fires before the request to load (or refresh) videoss from an album is sent
       */
      'before-load-videos',
      
      /**
       * @event videos-loaded
       * Fired whenever videos for a specific album have been retrieved
       * @param {Ext.data.Store} videos A store containing videos
       * @param {Kohive.models.VideoAlbum} album The album that videos were loaded for
       */
      'videos-loaded',
      
      /**
       * @event show-video
       * Fired whenever subscriber views should update the video they are displaying
       * @param {Kohive.models.Video} video The new video to display
       */
      'show-video',
      
      /**
       * @event video-destroyed
       * Fired when a video has been successfully destroyed
       * @param {Kohive.models.Video} video The video that was destroyed
       */
      'video-destroyed',
      
			/**
			 * @event video-created
			 * Fires when a video has been created using a URL
			 */
			'video-created',
			
			/**
			 * @event before-create-video
			 * Fires before an video is created from a Url
			 */
			'before-create-video',
			
			/**
			 * @event create-video-failed
			 * Fires when creating an video failed
			 */
			'create-video-failed',
			
      /**
       * @event before-update
       * Fired before an video update request is sent
       */
      'before-update',
      
      /**
       * @event update
       * Fired when an video has been successful updated
       * @param {Kohive.models.Video} video The video that was updated
       */
      'update',
      
      /**
       * @event before-create-album
       * Fires before the request to create an album is sent
       */
      'before-create-album',
      
      /**
       * @event album-created
       * Fired when a new album has been successfully created
       * @param {Kohive.models.Album} album The newly created album
       */
      'album-created',
      
      /**
       * @event rss-album-created
       * Fired when a new rss album has been successfully created
       * @param {Kohive.models.Album} album The newly created album
       */
      'rss-album-created',
      
      /**
       * @event album-destroyed
       * Fired when an album has been destroyed
       * @param {Kohive.models.Album} album The album that was destroyed
       */
      'album-destroyed'
    );
  },
	
	/**
	 * Add listeners to the controller
	 */
	initListeners: function() {
		// this.on('video-created', function() {
		// 	this.loadAlbumVideos(null, null, true);
		// }, this);
	}

});

Kohive.OS.registerController('videos', Kohive.app.VideosController);

Ext.ns('Kohive.views.videos');

/**
 * Kohive.Window
 * @extends Ext.Window
 * Provides customised Window behaviour for Kohive windows
 */
Kohive.Window = Ext.extend(Ext.Window, {
  
  originalStore: null,
  
  constructor: function(config){
    config = config || {};
    Ext.apply(config, {
      minWidth:        600,
      minHeight:       400
    });
    
    if (this.onStoreLoad) {
      this.onStoreLoad = this.onStoreLoad.createSequence(function (store) {
        this.updateFiltersTask = new Ext.util.DelayedTask(this.updateFilters, this);
        this.updateFiltersTask.delay(0);
      }, this);
    }
    
    Kohive.Window.superclass.constructor.call(this, config);
  },
  
  initComponent: function() {
    Ext.applyIf(this, {
      animateTarget:   Kohive.OS.getApplicationLauncher().el,
      closeAction:    'hide',
      constrain:       true,
      width:           635,
      height:          400,
      stateful:        false, // TODO, put this back when thigns are fixed when loading apps twice
      shadow:          'frame',
      shadowOffset:    20,
      autoScroll:      true,
      header:          true,
      layout:          'fit',
      hasDefaultTools: true,
      hbar:            true,
      bbar:            true
    });
		
		// this.maximizable = (this.initialConfig.maximizable === false) ? false : true;
		this.maximizable = false;

    // Add the dataview to the config only if it isn't been set
    if (!this.items) this.items = [this.getDataView()];

    // Check if hbar is defined and if it is true/array
    if (this.hbar && typeof this.hbar == "boolean") this.hbar = this.getDefaultHBar();

    // Check if bbar is defined and if it is true/array
    if (this.bbar && typeof this.bbar == "boolean") this.bbar = this.getDefaultStatusBar();

    // Check if panel is defined and if it is add the default panel
    if (this.filterPanelConfig && typeof this.filterPanelConfig != "object") this.filterPanelConfig = this.getDefaultPanel();

    //open the window in a random position if x and y not set
    if (this.x === undefined || this.y === undefined) {
      var vs = Ext.getBody().getViewSize();
      var viewWidth  = vs.width;
      var viewHeight = vs.height;

      var x = Math.max((Math.random() * viewWidth)  - this.width, 0)  + 92;
      var y = Math.max((Math.random() * viewHeight - 100) - this.height, 0) + 60;

      this.x = this.x === undefined? x : this.x;
      this.y = this.y === undefined? y : this.y;
    };

    //setup the header tools
		this.initHeaderTools();

    // ensure HeaderBar plugin installed if required
    if (this.hbar && !this.plugins) {
      this.plugins = [new Ext.ux.HeaderToolbar()];
    };

    // ensure WindowSidePanel plugin installed if required
    if (this.filterPanelConfig && !this.plugins) {
      this.plugins = [new Ext.ux.WindowSidePanel()];
    } else if (this.filterPanelConfig && this.plugins) {
      this.plugins.push(new Ext.ux.WindowSidePanel());
    };

    // check if the new form exists and if it does get it's event
    if (this.newForm) { this.relayEvents(this.newForm, ['create']); };
		
		this.on('render',   this.initBarMouseOver,   this);
		this.on('render',   this.checkIfMaximizable, this);
		this.on('render',   this.setupEditable,      this);
    this.on('render',   this.addStatusBarEvents, this);
    this.on('activate', this.addHistoryToken,    this);
    this.on('hide',     this.removeHistoryToken, this);
    
    Kohive.Window.superclass.initComponent.apply(this, arguments);
    
    this.initFilterListeners();
  },
  
  initFilterListeners: function() {
    if (this.store) {
      this.store.on('update', function(store, updatedRecord, method, name) {
        if (name != "toggled") {
          this.updateFiltersTask.delay(2000, this.updateFilters, this, [updatedRecord, method, name]);
        }
      }, this);
    }
  },
  
  updateFilters: function (updatedRecord, method, name) {
    Ext.each(this.windowSidePanel.filters, function (filter) {
      filter.clear();
    }, this);
    
    // Note that originalStore is not an Ext.data.Store :-)
    Ext.each(this.originalStore, function (record) {
      Ext.each(this.windowSidePanel.filters, function (filter) {
        filter.refresh(record, updatedRecord, method, name);
      }, this);
    }, this);
    
    // Ext.each(this.windowSidePanel.filters, function (filter) {
    //   filter.sort();
    // }, this);
    
    this.windowSidePanel.fireEvent('refreshed');
  },
	
	initBarMouseOver: function() {
		this.on('statusbar-event-start', function(msg) {
			this.showMessage(msg);
		}, this);
		
		this.on('statusbar-event-stop', this.hideLoading, this);
	},
	
	checkIfMaximizable: function() {
		if (!this.maximizable) {
			this.el.addClass('x-window-not-maximizable');
		};
	},
	
	/**
	 * 
	 */
	initHeaderTools: function() {
		//create the tools array if none is defined
		if (!this.tools) this.tools = [];
		
		//add default tools (which is a bug icon)
    if (this.hasDefaultTools) {
      this.tools.push({
        id:      'bug',
        handler: function() {
          feedback_widget.show();
        }
      });
    };
		
		//add the collapser thingy majigger if the header toolbar is defined
    if (this.hbar) {
      this.tools.push({
        id:   'collapse-header',
        scope: this,
        handler: function() {
          if (this.toggleHeaderToolbar) this.toggleHeaderToolbar();
        }
      });
    };
		
		//check if there is a help message defined
		if (this.initialConfig.helpMessage) {
			this.tools.push({
        id:      'help',
				cls:     'x-tools-help-btn',
				scope:   this,
        handler: this.showHelpHUD
      });
		};
	},
	
	/**
	 * 
	 */
	initHelpHud: function() {
		this.helpHud = new Ext.ux.HUD({
			cls:    'x-hud-help',
      html:   this.initialConfig.helpMessage,
      header: false,
      width:  'auto',
			poker:  false
		});
		
		this.helpHud.render();
		this.helpHud.hide();
	},
	
	/**
	 * 
	 */
	showHelpHUD: function() {
		//create the hud if not already done
		if (!this.helpHud) this.initHelpHud();
		
		//find the help button and get the element of it
		var el = this.el.child('.x-tool-help');
		
		//update the anchorToEl property and position/show it
		this.helpHud.anchorToEl = el;
		this.helpHud.showHUD([0, 0], 'tr-br', 1, null, false);
	},
  
  /**
   * A Ext.CompositeElement of all the pods in the application
   */
  pods: null,
  
  /**
   * The current toggle all state, defaults to false
   */
  toggleAllState: true,
  
  /**
   * True when something in the pod is currently being edited.
   */
  editing: false,
  
  /**
   * The current editable field
   */
  editableField: null,
  
  onEsc: Ext.emptyFn,
  
  /**
   * Function description
   * @param {Object} config Configuration properties for the dataview
   * @return {Ext.data.DataView} The dataview
   */
  getDataView: function(config) {
    var config = config || {};

    this.sortableDataView = new Ext.ux.SortableDataViewPanel({
      dataViewConfig:    Ext.applyIf(config, this.getDataViewConfig()),
      columnPanelConfig: this.columnPanelConfig || {},
      mainPanelBefore:   [this.newForm]
    });
    
    this.sortableDataView.columns.on('changesortorder', this.initPods, this);
    
    return this.sortableDataView;
  },
  
  /**
   * Returns the default dataview config
   * @return {Object} default dataview config
   */
  getDataViewConfig: function() {
    this.store.on('update', function(store) {
      this.fireEvent('update-filter-bar', store);
    }, this);
    
    this.store.on('beforeload', function(store) {
      this.setToggledStates(store);
    }, this);
    
    this.store.on('load', function(store) {
      this.toggleNewFormIfStoreIsEmpty();
      this.fireEvent('update-filter-bar', store);
      this.viewOnLoad(store);
      this.getToggledStates(store);
      if (this.initialConfig.hasTags === true) this.initTagsStore(store);
      
      //update the original store if the URL hasn't got a ? in it (filtered)
      if (store.proxy.conn.url.split('?').length == 1) {
        this.originalStore = store.data.items;
      };
		}, this);
		
    return {
      emptyText:    '<div class="no-results">' + this.getEmptyText() + '</div>',
      itemSelector: '.x-pod',
      overClass:    'x-over',
      tpl:          this.model.dataView,
      store:        this.store,
      listeners: {
        scope        : this,
        'mouseenter' : this.viewOnMouseOver,
        'mouseleave' : this.viewOnMouseOut,
        'click'      : this.viewOnClick
      }
    };
  },
	
	/**
   * Called before a record is updated
   */
  onBeforeUpdate: function() {
    this.showSubmitting();
  },
	
	/**
   * Creates a new Ext.ux.Editable instance bound to the dataview
   */
  setupEditable: function() {
		if (!this.sortableDataView) return;
		
    this.editable = new Ext.ux.Editable(this.sortableDataView.dataview, {
			view:         this, // Hack so functions like reply() (message board) can access the window view
			model:        this.model, // Hack so function like reply() will have the right scope
      clickActions: this.model.prototype.clickActions
    });
		
    this.editable.on('update', this.onBeforeUpdate, this);
    this.relayEvents(this.editable, ['update']);
    
		this.sortableDataView.dataview.store.on('load', this.initalizeEditableMouseover, this);
		
    this.showSuccess();
  },

	/**
   * Called whenever the user changes the filters applied in the filter bar. Fires the 'filter' event
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  onFiltersUpdated: function(filters) {
    // this.fireEvent('filter', filtersApplied);
		
		// TODO do this the proper way, until then, do without events..
		if (!this.store) return;
		
		//for some reason passing the params option to findAll isn't working, so build url manually :/
    var url        = Kohive.OS.scopedUrl(this.model.urlName || this.model.modelName.toLowerCase().pluralize()),
        conditions = [];
    
    //     tags       = filters.tags,
    //     authors    = filters.authors,
    //     recipients = filters.assignee_membership_id,
    //     priority   = filters.priority,
    //     effort     = filters.effort,
    //     type       = filters.type,
    //     flagged    = filters.flagged,
    //     state      = filters.state,
    //     milestones = filters.milestones,
    //     conditions = [];
    // 
    // if (tags && tags.length > 0)             conditions.push(String.format("tags={0}", tags.join(',')));
    // if (authors && authors.length > 0)       conditions.push(String.format("membership_ids={0}", authors.join(',')));
    // if (recipients && recipients.length > 0) conditions.push(String.format("recipient_membership_ids={0}", recipients.join(',')));
    // if (priority && priority.length > 0)     conditions.push(String.format("priority={0}", priority.join(',')));
    // if (effort && effort.length > 0)         conditions.push(String.format("effort={0}", effort.join(',')));
    // if (effort && effort.length > 0)         conditions.push(String.format("effort={0}", effort.join(',')));
    // if (type && type.length > 0)             conditions.push(String.format("type={0}", type.join(',')));
    // if (flagged && flagged.length > 0)       conditions.push(String.format("flagged={0}", flagged.join(',')));
    // if (state && state.length > 0)           conditions.push(String.format("state={0}", state.join(',')));
    // if (milestones && milestones.length > 0) conditions.push(String.format("milestones={0}", milestones.join(',')));
    
    //loop through each of the filters and add them to the conditions
    for (filter in filters) {
      if (filters[filter].length > 0) conditions.push(String.format(filter + "={0}", filters[filter].join(',')));
    };
		
		//update the stores url + reload the store
		if (conditions.length > 0) this.store.proxy.conn.url = url + '?' + conditions.join("&");
		else this.store.proxy.conn.url = url;
		
    this.store.load();
  },
  
  /**
   * Toggles the new form panel if the store is empty, when the store loads
   */
  toggleNewFormIfStoreIsEmpty: function() {
    if (this.initalized) return;
    
    // Check how many items is in the store, and then check if it equals to
    // 0. if it does, toggle the new form panel
    if (this.store.totalLength == 0) this.toggleNewForm.defer(50, this);
    
    this.initalized = true;
  },
  
  tagsStore: null,
	
	/**
	 * 
	 */
	initTagsStore: function(store) {
	  var array      = [],
	      storeArray = [];
	  
	  //loop through each of the records/tags
	  store.each(function(record) {
	    Ext.each(record.get('tags') || [], function(tag) {
	      if (array.indexOf(tag.name) == -1) array.push(tag.name);
	    }, this);
	  }, this);
	  
	  Ext.each(array, function(a) { storeArray.push([a]); }, this);
	  
	  var newStore = new Ext.data.SimpleStore({
      fields : ['tag'],
      data   : storeArray
    });
    
    this.tagsStore = newStore;
    
    if (this.newForm) this.newForm.updateTagsField(this.tagsStore);
	},
  
  /**
   * Sets the toggled field in each of the records to true/false depending
   * on the current toggleAllState
   */
  toggleAll: function() {
    // Hide them all
    if (this.toggleAllState) {
      this.store.each(function(record) {
        record.set('toggled', true);
      }, this);
      
      return this.toggleAllState = false;
    };
    
    // Show them all
    this.store.each(function(record) {
      record.set('toggled', false);
    }, this);
    
    return this.toggleAllState = true;
  },
  
  initalizeEditableMouseover: function() {
		// var dataview = this.sortableDataView.dataview;
		// if (!dataview) return;
		// 
		// Ext.each(dataview.getNodes(), function(node) {
		// 	var record = dataview.getRecord(node),
		// 			node	 = Ext.get(node),
		// 			els    = node.select('div');
		// 	
		// 	if (record.get('has_permission')) {
		// 		els.each(function(el) {
		// 			var element = Ext.get(el);
		// 			
		// 			Ext.each(element.dom.className.split(' '), function(cls) {
		// 				if (cls.split('-')[0] == "action") {
		// 					console.log(element.dom);
		// 					
		// 					element.on('mouseover', function() { console.log(element.dom); }, element);
		// 					// element.on('mouseout',  function() { this.removeClass('x-over'); });
		// 					
		// 					return;
		// 				};
		// 			}, this);
		// 		}, this);
		// 	};
			
			
		// 			var item = Ext.get(item);
		// 			
		// 			item.on('mouseover', item.addClass.createDelegate(item, ['x-over']));
		// 			item.on('mouseout',  item.removeClass.createDelegate(item, ['x-over']));
		// }, this);
		// 	};
		
		// if (!this.dataview) return;
		// 
		//     var tempTask = new Ext.util.DelayedTask(function() {
		// 	    var items  = this.dataview.el.select('.x-value');
		// 	
		// 	if (this.idCard && this.idCard.get('has_permission')) {
		// 		Ext.each(items.elements, function(item) {
		// 			var item = Ext.get(item);
		// 			
		// 			item.on('mouseover', item.addClass.createDelegate(item, ['x-over']));
		// 			item.on('mouseout',  item.removeClass.createDelegate(item, ['x-over']));
		// 		}, this);
		// 	};
		// 	  }, this);
		// 	  tempTask.delay(1000);
  },
  
  /**
   * Returns the empty text string which is currently used for the dataview
   * @return {String} Empty text string
   */
  getEmptyText: function() {
    return Ext.ux.Translator.translate('app_nothing_to_display');
  },
  
  /**
   * Returns the default hbar for the window
   */
  getDefaultHBar: function() {
    return [
      {
        text:    Ext.ux.Translator.translate('nnew'),
        iconCls: 'new',
        xtype:   'header_toolbar_button',
        handler: this.toggleNewForm,
        scope:   this
      },
      {
        text:    Ext.ux.Translator.translate('refresh'),
        iconCls: 'refresh',
        xtype:   'header_toolbar_button',
        handler: this.controller.fireAction.createDelegate(this.controller, ['refresh', this.controller])
      },
      {
        text:    Ext.ux.Translator.translate('toggle_all'),
        iconCls: 'toggle-all',
        xtype:   'header_toolbar_button',
        handler: this.toggleAll,
        scope:   this
      },
      '->',
      {
        xtype: 'searchfield',
				listeners: {
					scope: this,
					'keyup' : this.searchStore
				}
      }
    ];
  },
  
  /**
   * Gets the default window side panel content.
   * By default, it will listen for any datastores in the main window 
   */
  getDefaultPanel: function() {
    var config = config || {};
    
    // // Check if a store is defined, if not then we cannot do anything
    // // if (!this.store) { return config; };
    // 
    // /**
    //  * @property authorDataView
    //  * @type Ext.DataView
    //  * The dataview which will display the authors of all the currently loaded store items
    //  */
    // var authorDataView = new Ext.DataView({
    //   emptyText:    Ext.ux.Translator.translate('no_items_to_display'),
    //   singleSelect: true,
    //   autoHeight:   true,
    //   autoScroll:   true,
    //   overClass:    'x-view-over',
    //   itemSelector: 'div.item-wrap',
    //   store:        this.getAuthors(),
    //   tpl:          new Ext.XTemplate(
    //     '<tpl for=".">',
    //       '<div class="item-wrap">',
    //         '<div>{full_name}</div>',
    //       '</div>',
    //     '</tpl>'
    //   )
    // });
    // 
    // config.items = [authorDataView];
    
    return config;
  },
  
  /**
   * Looks at the current hive and get all the memberships from it, and returns it in a store
   */
  getAuthors: function() {
    var memberships = [];
    
    // We need to loop through each of these members and convert them so we have the
    // appropriate data (record.data)
    Ext.each(Kohive.OS.getCurrentHiveMemberships(), function(r) {
      // Get the records data
      var d = r.data;
      
      // Now each of the import field
      var array = [];
      array.push(d.id_card.nickname);
      array.push(d.id_card.email);
      array.push(String.format("{0} {1}", d.id_card.first_name, d.id_card.last_name));
      array.push(d.id_card_id);
      // array.push(d.state);
      
      memberships.push(array);
    }, this);
    
    return new Ext.data.SimpleStore({
      fields: ['nick_name', 'email', 'full_name', 'id_card_id'],
      data:   memberships,
      sortInfo: {
        field:     'full_name',
        direction: 'ASC'
      }
    });
  },
  
  /**
   * Method which maximizes the window the maximum viewport size possible
   */
  maximize: function() {
    if (!this.maximized) {
      this.expand(false);
      this.restoreSize = this.getSize();
      this.restorePos = this.getPosition(true);
      if (this.maximizable) {
        this.tools.maximize.hide();
        this.tools.restore.show();
      };
      this.maximized = true;
      this.el.disableShadow();
  
      if (this.dd) { this.dd.lock(); };
      if (this.collapsible) { this.tools.toggle.hide(); }
      this.el.addClass('x-window-maximized');
      this.container.addClass('x-window-maximized-ct');
      
      this.fitContainer();
      this.doConstrain();
      
      this.fireEvent('maximize', this);
      
      this.saveState();
    };
  },
  
  /**
   * Method which restores the window back to it's original size when the maximize button is
   * pressed again.
   */
  restore: function() {
    if (this.maximized) {
      this.el.removeClass('x-window-maximized');
      this.tools.restore.hide();
      this.tools.maximize.show();
      
      // Reposition the window
      var cp = this.getContainerConstrainPositions();
      var rp = { x: this.restorePos[0], y: this.restorePos[1] };
      
      if (rp.x < cp.x) { rp.x = cp.x; };
      if (rp.y < cp.y) { rp.y = cp.y; };
      
      this.setPosition(rp.x, rp.y);
      
      // Resize the window
      var vs = this.getContainerViewSize();
      var rs = this.restoreSize;
      
      // Check if the restore width or height is more than the viewports width or height
      if (rs.width > vs.width)   { rs.width = vs.width; };
      if (rs.height > vs.height) { rs.height = vs.height; };
      
      this.setSize(rs.width, rs.height);
      
      // Delete properties that aren't used anymore
      delete this.restorePos;
      delete this.restoreSize;
      
      this.maximized = false;
      this.el.enableShadow(true);
  
      if (this.dd) { this.dd.unlock(); };
      if (this.collapsible) { this.tools.toggle.show(); };
      this.container.removeClass('x-window-maximized-ct');
  
      this.doConstrain();
      
      this.fireEvent('restore', this);
      
      this.saveState();
    };
  },
  
  /**
   * Method which constrains the width and the height of the window depending on the size of the
   * viewport.
   */
  doConstrain: function() {
    // If the window is closed while the opening animation is running, don't run this method,
    // which makes the window appear at the end even though it was closed.
    // Especially ugly when switching hives while an app is opening
    if (this.hidden) return;
    
    // Resize and reposition the window
    var vs = this.getContainerViewSize();
    var cp = this.getContainerConstrainPositions();
    var ws = this.el.getBox();
    var bs = Ext.getBody().getBox();
    
    // Check for window side panel
    if (this.windowSidePanel) {
      if (!this.windowSidePanelHidden) {
        // bs.width = bs.width - this.windowSidePanel.el.getWidth();
      };
    };
    
    //position
    if (ws.x + ws.width > bs.width - 5) { ws.x = ws.x - ((ws.x + ws.width) - bs.width) - 5; };
    if (ws.y + ws.height > bs.height - 80) { ws.y = ws.y - ((ws.y + ws.height) - bs.height) - 80; };
    
    if (ws.x < cp.x) { ws.x = cp.x; };
    if (ws.y < cp.y) { ws.y = cp.y; };
    
    this.setPosition(ws.x, ws.y);
    
    //size
    if (ws.height == this.oldHeight + 1) { ws.height = this.oldHeight; };
    
    // Check if the restore width or height is more than the viewports width or height
    if (ws.width > vs.width)   { ws.width = vs.width; };
    if (ws.height > vs.height) { ws.height = vs.height; };
    
    this.setSize(ws.width, ws.height);
    
    this.saveState();
    
    this.oldHeight = ws.height;
  },
  
  /**
   * This is called internally by maximise.  We override it here to stop the window
   * taking up the whole screen when maximising
   */
  fitContainer: function() {
    var vs = this.getContainerViewSize();
    
    this.setSize(vs.width, vs.height - 120);
    this.setPosition(this.getContainerConstrainPositions());
  },
  
  /**
   * Returns the contrain properties for the x+y axis
   */
  getContainerConstrainPositions: function() {
    var ccp = { x: 50, y: 38 };
    
    // Check for window side panel
    if (this.windowSidePanel) {
      if (!this.windowSidePanelHidden) {
        var wspw = this.windowSidePanel.el.getWidth() + 10;
        ccp = { x: 50 + wspw, y: 38 };
      } else {
        ccp = { x: 60, y: 38 };
      };
    };
    
    return ccp;
  },
  
  /**
   * Gets the contains view size
   */
  getContainerViewSize: function() {
    var vs = this.container.getViewSize();
    
    vs.width  = vs.width - 55;
    vs.height = vs.height;
		
		// console.log(vs.height);
    
    // Check for window side panel
    if (this.windowSidePanel) {
      if (!this.windowSidePanelHidden) {
        var wspw = this.windowSidePanel.el.getWidth() + 10;
        vs.width = vs.width - wspw;
      } else {
        vs.width = vs.width - 10;
      };
    };
    
    return vs;
  },
  
  /**
   * Called whenever the window is activated, this adds an entry to the Ext.History singleton
   * so that the back button takes the user back to the previous window.  Only adds this window's
   * url if it is not already the current url. Also adds the current hive url slug to the history
   * token.
   */
  addHistoryToken: function() {
    var url = this.initialConfig.url || this.url;
    if (url && url != Ext.History.getToken()) {
      var hiveId = Kohive.OS.getCurrentHive().data.hive.id;
      
      if (hiveId) {
        Ext.History.add(hiveId + '/' + url);
      };
    };
  },
  
  /**
   * Called whenever the window is deactived/closed, this removes the bookmarks url from the history
   * token and returns the value back to just the url slug.
   */
  removeHistoryToken: function() {
    // Get the current hive ID
    var hiveId = Kohive.OS.getCurrentHive().data.hive.id;
    
    // Loop through each of the open windows and check if there is more than 1 open 
    var i = 0;
    Kohive.OS.getWindowManager().each(function(w) {
			if (!w.hidden) i++;
    }, this);
    
    // If the hive id has been set and there is only 1 window open, update the history token
    if (hiveId && i < 2) {
      return Ext.History.add(hiveId);
    };
  },
  
  // private
  initEvents : function(){
    Kohive.Window.superclass.initEvents.call(this);
		
		this.addEvents(
      /**
      * @event beforeclosebyuser
      * Fires before the window is closed by the user (via click on x button)
      */
      'beforeclosebyuser',
      
      /**
      * @event beforeclosebyuser
      * Fires after the window is closed by the user(via click on x button)
      */
      'closebyuser'
		);

    if(this.animateTarget){
      this.setAnimateTarget(this.animateTarget);
    }

    if(this.resizable){
      this.resizer = new Ext.Resizable(this.el, {
        minWidth: this.minWidth,
        minHeight:this.minHeight,
        handles: this.resizeHandles || "all",
        pinned: true,
        resizeElement : this.resizerAction
      });
      this.resizer.window = this;
      this.resizer.on("beforeresize", this.beforeResize, this);
    }

    if(this.draggable){
      this.dragEl = this.el.child('.x-window-tc');
      this.dragEl.dom.id = this.el.id + '-drag';
      this.dragEl.addClass('x-window-draggable');
    }
    this.initTools();

    this.el.on("mousedown", this.toFront, this);
    this.manager = this.manager || Ext.WindowMgr;
    this.manager.register(this);
    this.hidden = true;
    if(this.maximized){
      this.maximized = false;
      this.maximize();
    }
    if(this.closable){
      var km = this.getKeyMap();
      km.on(27, this.onEsc, this);
      km.disable();
    };
  },

	/**
   * Sets up listeners to events emitted by the application controller
   */
  initListeners: function() {
		this.on('render', this.setupEditable, this);
  },
  
  /**
   * Overwrite the addTool method so we can style it differently
   */
  addTool: function() {
    if (!this[this.toolTarget]) { // no where to render tools!
      return;
    };
    if (!this.toolTemplate){
      // initialize the global tool template on first use
      var tt = new Ext.Template(
         '<div class="x-tool x-tool-{id}">&#160;</div>'
      );
      tt.disableFormats = true;
      tt.compile();
      Ext.Panel.prototype.toolTemplate = tt;
    };
    if (!this.toolInnerTarget) {
      // Check if the inner tools element has been created yet
      this.toolInnerTarget = this.el.child('.x-window-tl').createChild({
        cls: 'x-tools'
      });
    };
    for (var i = 0, a = arguments, len = a.length; i < len; i++) {
      var tc = a[i];
      if (!this.tools[tc.id]){
        var target;
        if (tc.id == 'collapse-header') {
          target = this.el.child('.x-window-tl');
        } else {
          target = this.toolInnerTarget;
        };
        var overCls = 'x-tool-'+tc.id+'-over';
        var t = this.toolTemplate.insertFirst((tc.align !== 'left') ? target : target.child('span'), tc, true);
        this.tools[tc.id] = t;
        t.enableDisplayMode('block');
        t.unselectable();
        t.on('click', this.createToolHandler(t, tc, overCls, this));
        if (tc.on){
          t.on(tc.on);
        };
        if (tc.hidden) {
          t.hide();
        };
        if (tc.qtip) {
          if (typeof tc.qtip == 'object') {
            Ext.QuickTips.register(Ext.apply({
              target: t.id
            }, tc.qtip));
          } else {
            t.dom.qtip = tc.qtip;
          };
        };
        t.addClassOnOver(overCls);
      };
    };
  },
  
  /**
   * Slides the New Bookmark form into view depending on its display status
   * @param {Boolean} dontFocus True if you dont want to focus the first field
   */
  toggleNewForm: function(dontFocus) {
    var f = this.newForm || null;
    
    if (f) {
      f.toggleCollapse();
      f.doLayout();
			
			if (dontFocus === true) return;
			
			// Get the first item and focus on it
			var field = f.find()[0];
			if (field) {
				var tempTask = new Ext.util.DelayedTask(function() {
				  field.focus();
				}, this);
				tempTask.delay(500);
			};
    };
  },
  
  /**
   * Adds event listeners for the status bar so it can update when the events happen
   */
  addStatusBarEvents: function() {
    // If there is no statusbar, return
    if (!this.statusBar) { return; };
    
    // If there is no store, return
    if (!this.store) { return this.hideLoading(); };
    
    // Before the store loads, show the loading bookmarks text
    this.store.on('beforeload', function() {
      this.showLoading();
    }, this);
    
    // Once the store has loaded hide it
    this.store.on('load', function() {
      this.hideLoading();
      
      Ext.each(this.store.data.items, function(record) {
        var data = record.data;
        
        // Get the id card
        Ext.applyIf(data, {
          id_card: Kohive.OS.getIdCardFromMembershipId(data.author_membership_id)
        });
      });
    }, this);
    
    // if the store fails to load, show the error
    this.store.on('loadexception', function() {
      this.showFailure();
    }, this);
  },
  
  /**
   * Returns the default status bar for the window
   */
  getDefaultStatusBar: function() {
    this.statusBar = new Ext.StatusBar();
    this.showLoading();
    
    //add drag image
    this.on('render', function() {
      var ct      = this.el.child('.x-window-bwrap');
      if (!ct) return;
      
      this.dragger = ct.createChild({
        cls: 'x-drag-icon'
      });
    }, this);
    
    return this.statusBar;
  },
  
  /**
   * Convenience method for setting the status bar to loading
   * @param {String} message Custom loading message
   */
  showLoading: function(message) {
	  var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_submitting');
    this.statusBar.setStatus({
      text:    message,
      iconCls: 'loading-icon'
    });
  },
	
	showMessage: function(message) {
		if (!message) return;
		
		this.statusBar.addClass('x-hover');
		
    this.statusBar.setStatus({
      text:    message,
      iconCls: ''
    });
  },
  
  /**
   * Convenience method for removing the loading status from the status bar
   */
  hideLoading: function() {
		this.statusBar.removeClass('x-hover');
		
		// TODO: for some reason this is not working when changing hives. i'm guessing it is something to do
		//       with the hive windows closing, then it being called late or something...
		if (!this.statusBar || !this.statusBar.clearStatus) return;
		
    this.statusBar.clearStatus();
  },
  
  /**
   * Convenience method for setting the status bar to refreshing
   * @param {String} message custom message for the refreshing message
   */
  showRefreshing: function(message) {
		var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_submitting');
    this.showLoading(Ext.ux.Translator.translate('app_statusbar_refreshing'));
  },
  
  /**
   * Convenience method for setting the status bar to submitting
   * @param {String} message custom message for the refreshing message
   */
  showSubmitting: function(message) {
		var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_submitting');
    this.showLoading(message);
  },
  
  /**
   * Convenience method for setting the status bar to updating
   * @param {String} message custom message for the refreshing message
   */
  showUpdating: function(message) {
		var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_updating');
    this.showLoading(message);
  },
  
  /**
   * Convenience method for setting the status bar to creating
   * @param {String} message custom message for the refreshing message
   */
  showCreating: function(message) {
		var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_creating');
    this.showLoading(message);
  },
  
  /**
   * Convenience method for setting the status bar to destroying
   * @param {String} message custom message for the refreshing message
   */
  showDestroying: function(message) {
		var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_destroying');
    this.showLoading(message);
  },
  
  /**
   * Convenience method for setting the status bar to success
   * @param {String} message Custom loading message
   */
  showSuccess: function(message) {
		var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_success');
    this.statusBar.setStatus({
      text:    message,
      iconCls: 'success-icon'
    });

    var tempTask = new Ext.util.DelayedTask(function() {
      this.hideLoading();
    }, this);
    tempTask.delay(5000);
  },
  
  /**
   * Convenience method for setting the status bar to failure
   * @param {String} message Custom loading message
   */
  showFailure: function(message) {
		var message = (message && typeof message == "string") ? message : Ext.ux.Translator.translate('app_statusbar_failure');
    this.statusBar.setStatus({
      text:    message,
      iconCls: 'failure-icon'
    });
    
    var tempTask = new Ext.util.DelayedTask(function() {
      this.hideLoading();
    }, this);
    tempTask.delay(5000);
  },
  
  createGhost: function(cls, useShim, appendTo){
    var el = document.createElement('div');
    el.id = this.el.id + '-ghost';
    el.className = 'x-panel-ghost ' + (cls ? cls : '');
    
    if (this.header) {
      el.appendChild(this.el.dom.firstChild.cloneNode(true));
    };
    
    // Calculate the height of the window
    var height = 0;
    // if (this.header || this.hbar) { height = height + this.el.child('.x-window-tl').getHeight(); };
    height = height + this.bwrap.getHeight();
    
    if (this.hbar) {
      height = height + this.el.child('.x-window-toolbar-r').getHeight();
      Ext.fly(el).child('.x-window-toolbar-r').remove();
    };
    
    // Set the height and width of the ghost
    Ext.fly(el).setHeight(height);
    var ghost = Ext.fly(el.appendChild(document.createElement('ul'))).setHeight(height);
    el.style.width = this.el.dom.offsetWidth + 'px';
    
    this.container.dom.appendChild(el);
    
    // Check for window side panel
    if (this.windowSidePanel) {
      if (!this.windowSidePanelHidden) {
        var ghostSide = Ext.fly(el.appendChild(document.createElement('ul'))).setHeight(this.windowSidePanel.el.getHeight()).setWidth(this.windowSidePanel.el.getWidth()).addClass('sidePanel');
        var thisEl = Ext.get(el.id);
        // console.log(thisEl);
        ghostSide.anchorTo(thisEl, 'r-l', [0, 25]);
      };
    };
    
    if (useShim !== false && this.el.useShim !== false) {
      var layer = new Ext.Layer({shadow:false, useDisplay:true, constrain:false}, el);
      layer.show();
      return layer;
    } else {
      return new Ext.Element(el);
    };
  },
  
  /**
   * Wrapper around hide method, to fire a special event when window is closed (hidden) by user to differentiate
   * between hidden by user and hidden by system (when switching hives)
   */
  closeByUser : function(){
    if (this.fireEvent("beforeclosebyuser", this) !== false){
      this.hide();
      this.fireEvent('closebyuser', this);
    }
  },

	/**
	 * Searching the store
	 */
	searchStore: function(field, value) {
		var value = (value === true) ? field : field.getValue();
		
		//check if store exists
		if (!this.store) return;
		
		//check if the value has spaces, if it does change it into an array
		//TODO add implementation for ||
		if (value.split(' ').length > 1) value = value.split(' ');
		
		//clear the current filters
		this.store.clearFilter();
		
		//filter the store
		this.store.filterBy(function(record, id) {
			var ignoreFields = ['id', 'hive_id', 'can_edit', 'can_delete', 'created_at', 'updated_at', 'modified_at', 'tags'],
					result       = false;
			
			if (typeof value == "object") {
			  var str = "";
			  Ext.each(value, function(v, i) {
			    //add the value to the str
			    str += v;
			    
			    //add the | if it isnt the last one
			    if (i != value.length - 1) {
			      if (value[i + 1] != "") str += "|";
			    };
			  }, this);
			  
			  regExp = new RegExp(str, 'i');
			} else {
			  regExp = new RegExp(value, 'i');
			};
			
			record.fields.each(function(field) {
				if (result === true) return;
				if (ignoreFields.indexOf(field.name) == -1) if (regExp.test(record.get(field.name))) result = true;
			}, this);
			
			return result;
		}, this);
	},
	
	/**
   * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
	 * @param {Integer} index The index of the node in question
	 * @param {HTMLElement} node The node in question
	 * @param {Ext.EventObject} event The event fired
	 */
	viewOnMouseOver: function(dataview, index, node, el) {
	  var el     = Ext.get(node),
	      record = dataview.getRecord(node);
	  
    
	},
	
	/**
	 * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
 	 * @param {Integer} index The index of the node in question
 	 * @param {HTMLElement} node The node in question
 	 * @param {Ext.EventObject} event The event fired
	 */
	viewOnMouseOut: function(dataview, index, node, el) {
	  var el     = Ext.get(node),
	      record = dataview.getRecord(node);
	  
    
	},
	
	/**
	 * Called when the windows dataview has been clicked. it has to do a few things:
	 *  - toggle the pod
	 *  - delete stuff
	 * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
   * @param {Integer} index The index of the node in question
   * @param {HTMLElement} node The node in question
   * @param {Ext.EventObject} event The event fired
	 */
	viewOnClick: function(dataview, index, node, e) {
	  var el     = Ext.get(node),
	      target = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
	  
	  //Toggle
	  if (target.hasClass('x-control') || target.parent().hasClass('x-control')) {
	    (record.get('toggled') === true) ? record.set('toggled', false) : record.set('toggled', true);
	  };
	  
	  //Delete
	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) {
      var title = el.child('.x-title div');
      if (title) title = title.dom.innerHTML;
      
      var contentEl = el.child('.x-content-wrap') || el;
      
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('app_' + this.model.modelName.toLowerCase().singularize() + '_destroy_desc', { title: title || null }),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") this.controller.fireAction('destroy', this.controller, [record, this.store, contentEl]);
        }
      });
	  };
	},
	
	/**
	 *  Method called when the dataview store has been loaded.
	 * 
	 * @param {Ext.data.Store} store The store which has been loaded
	 */
	viewOnLoad: function(store) {
	  var dataview = this.sortableDataView.dataview;
	  if (!dataview) return;
	  
	  var nodes = dataview.getNodes();
	  
	  Ext.each(nodes, function(node) {
	    var record = dataview.getRecord(node),
	        el     = Ext.get(node);
	    
      if (record.get('can_edit')) {
        // editable rollovers
        var items  = el.select('div'),
            fields = [];

        Ext.each(items.elements || [], function(item) {
         var item = Ext.get(item);
 
         Ext.each(item.dom.className.split(' '), function(cls) {
           if (cls.split('-')[0] == "action") fields.push(item);
         }, this);
        }, this);

        Ext.each(fields, function(field) {
          field.on('mouseover', field.addClass.createDelegate(field, ['x-over']));
          field.on('mouseout',  field.removeClass.createDelegate(field, ['x-over']));
        }, this);
      };
    }, this);
	},
	
	/**
	 * Stores an array of all toggled records in the current store.
	 * The index will contain the id of the record, and the value will contain the 'toggled' param
	 */
	currentToggledStates: [],
	
	/**
	 * Method called on 'beforeload' of the store in the Ext.Window
	 * Loops through each of the records and saves the 'toggled' state into the currentToggledStates
	 * property of the window.
	 * @param {Ext.data.Store} store The current store (before load)
	 */
	setToggledStates: function(store) {
	  if (!store) return;
	  
	  this.currentToggledStates = [];
	  store.each(function(record) {
	    this.currentToggledStates[record.get('id')] = record.get('toggled');
	  }, this);
	},
	
	/**
	 * Method called when the store has been loaded ('load')
	 * Loops through each of the records in the store and checks if any records have a toggled state
	 * saved in the windows currentToggledStates property and set's it if nessecary
	 * @param {Ext.data.Store} store The new store (after load)
	 */
	getToggledStates: function(store) {
	  if (!store || this.currentToggledStates.length == 0) return;
	  
    store.each(function(record) {
      var id = record.get('id');
      if (this.currentToggledStates[id] === true || this.currentToggledStates[id] === false) record.set('toggled', this.currentToggledStates[id]);
    }, this);
	}
});

/**
 * Overwrite the default drag implementation
 */
Ext.Window.DD = function(win) {
  this.win = win;
  Ext.Window.DD.superclass.constructor.call(this, win.el.id, 'WindowDD-' + win.id);
  
  this.setHandleElId(win.id + '-drag');
  this.scroll = false;
};
Ext.extend(Ext.Window.DD, Ext.dd.DD, {
  moveOnly:      true,
  startDrag:     function() {
    var w = this.win;
    
    this.proxy = w.ghost();
    
    var so = w.el.shadowOffset;
    
    // Check for window side panel
    if (w.windowSidePanel) {
      if (!w.windowSidePanelHidden) {
        var wspw = w.windowSidePanel.el.getWidth() + 10;
        this.constrainTo(w.container, {top: 38, right: 5, left: 50 + wspw, bottom: 80});
      } else {
        this.constrainTo(w.container, {top: 38, right: 5, left: 60, bottom: 80});
      };
    } else {
      this.constrainTo(w.container, {top: 38, right: 5, left: 50, bottom: 80});
    };
  },
  
  b4Drag: Ext.emptyFn,

  onDrag: function(e) {
    this.alignElWithMouse(this.proxy, e.getPageX(), e.getPageY());
  },

  endDrag: function(e) {
    // Remove the ghost
    this.win.unghost();
    
    // Update the state
    this.win.saveState();
    
    // doLayout
    // this.win.doLayout();
  }
});

Ext.reg('kohive_window', Kohive.Window);

/**
 * @class Kohive.FormPanel
 * @extends Ext.form.FormPanel
 * Customizes the Ext.form.FormPanel with a new template
 */
Kohive.FormPanel = function(config) {
  var config = config || {};
  
  this.addEvents(
    /**
     * @event create
     * Fires when the 'Add' button is clicked indicating the user wants to save the new Bookmark
     * @param {Ext.form.FormPanel} this The form panel
     */
    'create'
  );
  
  Ext.applyIf(config, {
    // hidden:     true,
    cls:        'x-kohive-form',
    labelWidth: 60,
    labelAlign: 'right',
    defaults: {
      anchor: '100%'
    },
    buttonAlign: 'right'
  });
  
  Kohive.FormPanel.superclass.constructor.call(this, config);
  
  this.on('create', this.beforeSubmit, this);
};
Ext.extend(Kohive.FormPanel, Ext.form.FormPanel, {
  // private
  onCollapse: function(doAnim, animArg) {
    if (doAnim) {
      this.el.slideOut(this.slideAnchor, Ext.apply(this.createEffect(animArg||true, this.afterCollapse, this), this.collapseDefaults));
    } else {
      this.el.hide();
      this.afterCollapse();
    };
  },
  
  // private
  onExpand: function(doAnim, animArg) {
    if (doAnim) {
      this.el.slideIn(this.slideAnchor, Ext.apply(this.createEffect(animArg||true, this.afterExpand, this), this.expandDefaults));
    } else {
      this.el.show();
      this.afterExpand();
    };
  },
  
  /**
   * Delegates the form/control errors
   * @param {Object} errors An object will errors inside it
   * @return {String} Return value description
   */
  delegateErrors: function(errors) {
    // console.log('delegate');
    if (!errors) {
      return;
    };
  },
  
  /**
   * Highlights the field with correct styling when it has an error
   */
  showFieldError: function(error) {
    var nId = error.name;
    var k   = error.key;
    
    if (k == 'validation.required') {
      var n = Ext.get(nId);
      
      if (n) {
        n.addClass('x-form-invalid');
      };
    };
  },
  
  /**
   * Checks if all the form items are valid
   * @return {Boolean} Returns true/false
   */
  isValid: function() {
    var valid = true;
    
    // Loop through each of the form items and check if they are valid
    this.items.each(function(f) {
      f.validate();
      if (f.isValid && !f.isValid(true)) {
        valid = false;
      }
    });
    
    // If anything is invalid, return false
    if (!valid) {
      this.afterSubmit();
      return false;
    };
    
    return true;
  },
  
  /**
   * Is called before anything is submited
   */
  beforeSubmit: function() {
    this.btn.disable();
  },
  
  /**
   * Is called after the form has been subitted
   */
  afterSubmit: function() {
    this.btn.enable();
  },
  
  /**
   * Function description
   */
  getAddButton: function() {
    /**
     * Kohive button which is used in the form
     */
    this.btn = new Kohive.SmallButton({
      text:   'Add',
      scope:   this,
      handler: this.fireEvent.createDelegate(this, ['create', this])
    });
    
    return this.btn;
  }
});

Ext.reg('kohive_form_panel', Kohive.FormPanel);

Ext.MessageBox = function(){
    var dlg, opt, mask, waitTimer;
    var bodyEl, msgEl, textboxEl, textareaEl, progressBar, pp, iconEl, spacerEl;
    var buttons, activeTextEl, bwidth, iconCls = '';

        var handleButton = function(button){
        if(dlg.isVisible()){
            dlg.hide();
            Ext.callback(opt.fn, opt.scope||window, [button, activeTextEl.dom.value], 1);
        }
    };

        var handleHide = function(){
        if(opt && opt.cls){
            dlg.el.removeClass(opt.cls);
        }
        progressBar.reset();
    };

        var handleEsc = function(d, k, e){
        if(opt && opt.closable !== false){
            dlg.hide();
        }
        if(e){
            e.stopEvent();
        }
    };

        var updateButtons = function(b){
        var width = 0;
        if(!b){
            buttons["ok"].hide();
            buttons["cancel"].hide();
            buttons["yes"].hide();
            buttons["no"].hide();
            return width;
        }
        dlg.footer.dom.style.display = '';
        for(var k in buttons){
            if(typeof buttons[k] != "function"){
                if(b[k]){
                    buttons[k].show();
                    buttons[k].setText(typeof b[k] == "string" ? b[k] : Ext.MessageBox.buttonText[k]);
                    width += buttons[k].el.getWidth()+15;
                }else{
                    buttons[k].hide();
                }
            }
        }
        return width;
    };

    return {
        
        getDialog : function(titleText){
           if(!dlg){
                dlg = new Ext.Window({
                    autoCreate : true,
                    title:titleText,
                    resizable:false,
                    constrain:true,
                    constrainHeader:true,
                    minimizable : false,
                    maximizable : false,
                    stateful: false,
                    modal: true,
                    shim:true,
                    buttonAlign:"center",
                    width:400,
                    height:100,
                    minHeight: 80,
                    plain:true,
                    footer:true,
                    closable:false,
                    // close : function(){
                    //     if(opt && opt.buttons && opt.buttons.no && !opt.buttons.cancel){
                    //         handleButton("no");
                    //     }else{
                    //         handleButton("cancel");
                    //     }
                    // },
                    addButton: function(config, handler, scope) {
                      if (scope && typeof scope == 'string') {
                        var bc = {
                          handler: handler,
                          scope: this,
                          minWidth: this.minButtonWidth,
                          hideParent:true
                        };
                        if (typeof config == "string"){
                          bc.text = config;
                        } else {
                          Ext.apply(bc, config);
                        };
                        var btn = new Kohive.Button(bc);
                        btn.ownerCt = this;
                        if (!this.buttons) {
                          this.buttons = [];
                        };
                        this.buttons.push(btn);
                        return btn;
                      };
                      
                      var bc = {
                        handler: handler,
                        scope: scope,
                        minWidth: this.minButtonWidth,
                        hideParent:true
                      };
                      if (typeof config == "string"){
                        bc.text = config;
                      } else {
                        Ext.apply(bc, config);
                      };
                      var btn = new Ext.Button(bc);
                      btn.ownerCt = this;
                      if (!this.buttons) {
                        this.buttons = [];
                      };
                      this.buttons.push(btn);
                      return btn;
                    }
                });
                buttons = {};
                var bt = this.buttonText;
                
                buttons["ok"] = dlg.addButton(bt["ok"], handleButton.createCallback("ok"), 'kohive_button');
                buttons["yes"] = dlg.addButton({ text: bt["yes"], color: 'red' }, handleButton.createCallback("yes"), 'kohive_button');
                buttons["no"] = dlg.addButton({ text: bt["no"], color: 'green' }, handleButton.createCallback("no"), 'kohive_button');
                buttons["cancel"] = dlg.addButton({ text: bt["cancel"], color: 'red' }, handleButton.createCallback("cancel"), 'kohive_button');
                buttons["ok"].hideMode = buttons["yes"].hideMode = buttons["no"].hideMode = buttons["cancel"].hideMode = 'offsets';
                
                dlg.render(document.body);
                dlg.getEl().addClass('x-window-dlg');
                mask = dlg.mask;
                bodyEl = dlg.body.createChild({
                    html:'<div class="ext-mb-icon"></div><div class="ext-mb-content"><span class="ext-mb-text"></span><br /><div class="ext-mb-fix-cursor"><input type="text" class="ext-mb-input" /><textarea class="ext-mb-textarea"></textarea></div></div>'
                });
                iconEl = Ext.get(bodyEl.dom.firstChild);
                var contentEl = bodyEl.dom.childNodes[1];
                msgEl = Ext.get(contentEl.firstChild);
                textboxEl = Ext.get(contentEl.childNodes[2].firstChild);
                textboxEl.enableDisplayMode();
                textboxEl.addKeyListener([10,13], function(){
                    if(dlg.isVisible() && opt && opt.buttons){
                        if(opt.buttons.ok){
                            handleButton("ok");
                        }else if(opt.buttons.yes){
                            handleButton("yes");
                        }
                    }
                });
                textareaEl = Ext.get(contentEl.childNodes[2].childNodes[1]);
                textareaEl.enableDisplayMode();
                progressBar = new Ext.ProgressBar({
                    renderTo:bodyEl
                });
               bodyEl.createChild({cls:'x-clear'});
            }
            return dlg;
        },

        
        updateText : function(text){
            if(!dlg.isVisible() && !opt.width){
                dlg.setSize(this.maxWidth, 100);             }
            msgEl.update(text || '&#160;');

            var iw = iconCls != '' ? (iconEl.getWidth() + iconEl.getMargins('lr')) : 0;
            var mw = msgEl.getWidth() + msgEl.getMargins('lr');
            var fw = dlg.getFrameWidth('lr');
            var bw = dlg.body.getFrameWidth('lr');
            if (Ext.isIE && iw > 0){
                                                iw += 3;
            }
            var w = Math.max(Math.min(opt.width || iw+mw+fw+bw, this.maxWidth),
                        Math.max(opt.minWidth || this.minWidth, bwidth || 0));

            if(opt.prompt === true){
                activeTextEl.setWidth(w-iw-fw-bw);
            }
            if(opt.progress === true || opt.wait === true){
                progressBar.setSize(w-iw-fw-bw);
            }
            dlg.setSize(w, 'auto').center();
            return this;
        },

        
        updateProgress : function(value, progressText, msg){
            progressBar.updateProgress(value, progressText);
            if(msg){
                this.updateText(msg);
            }
            return this;
        },

        
        isVisible : function(){
            return dlg && dlg.isVisible();
        },

        
        hide : function(){
            if(this.isVisible()){
                dlg.hide();
                handleHide();
            }
            return this;
        },

        
        show : function(options){
            if(this.isVisible()){
                this.hide();
            }
            opt = options;
            var d = this.getDialog(opt.title || "&#160;");

            d.setTitle(opt.title || "&#160;");
            var allowClose = (opt.closable !== false && opt.progress !== true && opt.wait !== true);
            // d.tools.close.setDisplayed(allowClose);
            activeTextEl = textboxEl;
            opt.prompt = opt.prompt || (opt.multiline ? true : false);
            if(opt.prompt){
                if(opt.multiline){
                    textboxEl.hide();
                    textareaEl.show();
                    textareaEl.setHeight(typeof opt.multiline == "number" ?
                        opt.multiline : this.defaultTextHeight);
                    activeTextEl = textareaEl;
                }else{
                    textboxEl.show();
                    textareaEl.hide();
                }
            }else{
                textboxEl.hide();
                textareaEl.hide();
            }
            activeTextEl.dom.value = opt.value || "";
            if(opt.prompt){
                d.focusEl = activeTextEl;
            }else{
                var bs = opt.buttons;
                var db = null;
                if(bs && bs.ok){
                    db = buttons["ok"];
                }else if(bs && bs.yes){
                    db = buttons["yes"];
                }
                if (db){
                    d.focusEl = db;
                }
            }
            if(opt.iconCls){
              d.setIconClass(opt.iconCls);
            }
            this.setIcon(opt.icon);
            bwidth = updateButtons(opt.buttons);
            progressBar.setVisible(opt.progress === true || opt.wait === true);
            this.updateProgress(0, opt.progressText);
            this.updateText(opt.msg);
            if(opt.cls){
                d.el.addClass(opt.cls);
            }
            d.proxyDrag = opt.proxyDrag === true;
            d.modal = opt.modal !== false;
            d.mask = opt.modal !== false ? mask : false;
            if(!d.isVisible()){
                                document.body.appendChild(dlg.el.dom);
                d.setAnimateTarget(opt.animEl);
                d.show(opt.animEl);
            }

                        d.on('show', function(){
                // if(allowClose === true){
                //     d.keyMap.enable();
                // }else{
                //     d.keyMap.disable();
                // }
            }, this, {single:true});

            if(opt.wait === true){
                progressBar.wait(opt.waitConfig);
            }
            return this;
        },

        
        setIcon : function(icon){
            if(icon && icon != ''){
                iconEl.removeClass('x-hidden');
                iconEl.replaceClass(iconCls, icon);
                iconCls = icon;
            }else{
                iconEl.replaceClass(iconCls, 'x-hidden');
                iconCls = '';
            }
            return this;
        },

        
        progress : function(title, msg, progressText){
            this.show({
                title : title,
                msg : msg,
                buttons: false,
                progress:true,
                closable:false,
                minWidth: this.minProgressWidth,
                progressText: progressText
            });
            return this;
        },

        
        wait : function(msg, title, config){
            this.show({
                title : title,
                msg : msg,
                buttons: false,
                closable:false,
                wait:true,
                modal:true,
                minWidth: this.minProgressWidth,
                waitConfig: config
            });
            return this;
        },

        
        alert : function(title, msg, fn, scope){
            this.show({
                title : title,
                msg : msg,
                buttons: this.OK,
                fn: fn,
                scope : scope
            });
            return this;
        },

        
        confirm : function(title, msg, fn, scope){
            this.show({
                title : title,
                msg : msg,
                buttons: this.YESNO,
                fn: fn,
                scope : scope,
                icon: this.QUESTION
            });
            return this;
        },

        
        prompt : function(title, msg, fn, scope, multiline, value){
            this.show({
                title : title,
                msg : msg,
                buttons: this.OKCANCEL,
                fn: fn,
                minWidth:250,
                scope : scope,
                prompt:true,
                multiline: multiline,
                value: value
            });
            return this;
        },

        
        OK : {ok:true},
        
        CANCEL : {cancel:true},
        
        OKCANCEL : {ok:true, cancel:true},
        
        YESNO : {yes:true, no:true},
        
        YESNOCANCEL : {yes:true, no:true, cancel:true},
        
        INFO : 'ext-mb-info',
        
        WARNING : 'ext-mb-warning',
        
        QUESTION : 'ext-mb-question',
        
        ERROR : 'ext-mb-error',

        
        defaultTextHeight : 75,
        
        maxWidth : 600,
        
        minWidth : 100,
        
        minProgressWidth : 250,
        
        buttonText : {
            ok : "OK",
            cancel : "Cancel",
            yes : "Yes",
            no : "No"
        }
    };
}();

Ext.Msg = Ext.MessageBox;

Ext.ns('Kohive.views.pod');

/**
 * Kohive.views.pod.Index
 * @extends Kohive.Window
 * Default pod view for applications
 */
Kohive.views.pod.Index = function(model, config) {
  var config = config || {};
  
  this.model = model;
  this.os    = ExtMVC.OS.getOS();
  
  // get the controller name + the controller
  this.controllerName = model.prototype.modelName.pluralize();
  this.controller     = this.os.getController(this.controllerName);
  
  // get the application name from the controller
  this.applicationName = this.controllerName.toLowerCase().replace(" ", "");
  
  // Find all the stuff in the store if a store is not set by default
  config.store = config.store || model.findAll({
    url: '/memberships/' + Kohive.OS.getCurrentHive().data.id + '/' + this.model.urlName,
		sortInfo: {
			field:     'created_at',
			direction: 'DESC'
		},
    listeners: {
      'load': {
        scope: this,
        fn: function(store) {
          this.cachedRecords = store;
        }
      }
    }
  });
	
	// config.store.on('load', this.fireEventForAppLauncher, this);
	
	config.filterPanelConfig = config.filterPanelConfig || {
    filters: [
      // {
      //         ftype: 'tags',
      //         listenTo: {
      //           component: this,
      //           events:    ['update-filter-bar']
      //         }
      //       },
      //       {
      //         ftype: 'authors',
      //        listenTo: {
      //           component: this,
      //           events:    ['update-filter-bar']
      //         }
      //       }
      {
        ftype:        'store-helper',
        displayField: 'tags.name',
        valueField:   'tags.name',
        title:        Ext.ux.Translator.translate('tags'),
        iconCls:      'tag',
        view:         this,
        listenTo: {
          component: config.store,
          events:    ['load', 'update']
        }
      },
      {
        ftype:        'store-helper',
        displayField: 'author_nickname',
        valueField:   'author_membership_id',
        title:        Ext.ux.Translator.translate('authors'),
        iconCls:      'author',
        view:         this,
        listenTo: {
          component: config.store,
          events:    ['load', 'update']
        }
      }
    ],
    listeners: {
      scope            : this,
      'filters-updated': this.onFiltersUpdated
    }
  };
  
  // Get the applications click actions from the model, if none are set
  config.clickActions = config.clickActions || model.clickActions;
  
  /**
   * @property cachedRecords
   * @type Array
   * Maintains a cache of loaded Tasks so that we can easily access the underlying
   * Ext.data.Record object for each when listening for clicks in the DataView
   */
  this.cachedRecords = [];

  /**
   * @property newTasksForm
   * @type Ext.form.FormPanel
   * Reference to the new bookmark form used internally by this view
   */
  this.newForm = new Kohive.views[this.applicationName].New();
  
  /**
   * @property columnPanelConfig
   * @type Object
   * The columns which are configured for the dataview. Reverts to the default if none are set.
   */
  this.columnPanelConfig = config.columnPanelConfig || this.getDefaultColumnPanelConfig();
  
  /**
   * @property emptyText
   * @type String
   * The empty text to be used on the dataview when there is no items in te store. If nothing is
   * set it defaults to 'no results'
   */
  this.emptyText = Ext.ux.Translator.translate('app_' + this.applicationName + '_no_results');
  
  // Configure the new Kohive Window for the application
  Ext.applyIf(config, {
    title: Ext.ux.Translator.translate('app_' + this.applicationName),    
    id:    this.applicationName + '_index_' + Kohive.OS.getCurrentHive().data.hive_id,
    cls:   this.applicationName,
    url:   this.applicationName
  });
  
  Ext.applyIf(this, {
    store: config.store
  });
 
  Kohive.views.pod.Index.superclass.constructor.call(this, config);
  
  // Build the events for the view
  this.buildEvents(config.clickActions);
};

Ext.extend(Kohive.views.pod.Index, Kohive.Window, {
  closeAction: 'hide',
  
	fireEventForAppLauncher: function() {
		// this.controller.fireEvent('refresh');
		// this.store.un('load', this.fireEventForAppLauncher, this);
	},
	
  /**
   * Adds the appropriate events for each of the specified click actions
   * @param {Array} actions An array of click actions to be used on the application view
   */
  buildEvents: function(actions) {
    if (!actions) { return; };
    
    // Events array
    var events = [];
    
    // Loop through each of the actions
    for (var i = 0; i < actions.length; i++) {
      // Get the action
      var action = actions[i];
      
      // Cant do anything if no name has been set
      if (!action.name) { return; };
      
      var actionName;
      
      // Check if it is an edit action
      if (action.edit) {
        actionName = 'action-edit-' + action.name;
      } else {
        actionName = 'action-' + action.name;
      };
      
      // Push the action name into the events array
      events.push(actionName);
    };
    
    // Add the events
    this.addEvents(events);
  },
  
  /**
   * Returns the default column panel configuration for the date view.
   * @return {Object} Returns the default columns for the dataview
   */
  getDefaultColumnPanelConfig: function() {
    return {
      items: [
        {field: 'created_at',      text: Ext.ux.Translator.translate('date')},
        {field: 'author_nickname', text: Ext.ux.Translator.translate('creator')}
      ]
    };
  }
});
Ext.reg('kohive_views_pod_index', Kohive.views.pod.Index);

/**
 * @class Kohive.views.pod.New
 * @extends Ext.form.FormPanel
 * Base class for any scaffold form panel (e.g. new and edit forms)
 */
Kohive.views.pod.New = Ext.extend(Ext.form.FormPanel, {
  /**
   * Sets up the FormPanel, adds default configuration and items
   */
  constructor: function(model, config) {
    var config = config || {};
    
    this.model = model;
    this.os    = ExtMVC.OS.getOS();
    
    this.controllerName = config.controllerName || this.model.modelName.pluralize().toLowerCase();
    this.controller     = this.os.getController(this.controllerName);
    
    /**
     * @property submitButton
     * @type Ext.Button
     * The default 'add' button reference
     */
    this.submitButton = this.submitButton || this.buildSubmitButton(config.submitButtonConfig);
    
    Ext.applyIf(config, {
      title:      Ext.ux.Translator.translate('nnew'),
      autoScroll: true,
      labelAlign: 'right',
      collapsed:  true,
      labelWidth: 75,
      cls:        'x-pod-new-form',
      
      keys: [
        {
          key :      Ext.EventObject.ENTER,
          scope:     this,
          handler:   function(key, e) {
            var target = e.getTarget();
            if (target.type == "text") {
              this.onCreate();
            };
          }
        },
        {
          key:       Ext.EventObject.ENTER,
          ctrl:      true,
          scope:     this,
          stopEvent: true,
          handler:   this.onCreate
        }
      ],
      
      buttonAlign: 'right',
      buttons: [this.submitButton]
    });
    
    Kohive.views.pod.New.superclass.constructor.call(this, config);
    
    this.initListeners();
  },
  
  collapseEl: 'el',
  
  initListeners: function() {
    this.on('render', this.listenToChildren, this);
  },
  
  /**
   * Creates a default 'Add' button. Override this if needed in subclasses, usually
   * this will do the trick though
   * @param {Object} config Button config
   * @return {Kohive.SmallButton} The button instance
   */
  buildSubmitButton: function(config) {
		var config = Ext.applyIf(config || {}, {
      text:     Ext.ux.Translator.translate('add'),
      scope:    this,
      disabled: true,
      handler:  this.onCreate
    });
		
    return new Kohive.SmallButton(config);
  },
  
  /**
   * Adds default items, keys and buttons to the form
   */
  initComponent: function() {
    // Check if items is defined
    if (!this.items) { this.items = this.buildItems(this.model); };
    
    Kohive.views.pod.New.superclass.initComponent.apply(this, arguments);
    
    this.on('render', function() {
      this.createDeleteButton();
      
      var tempTask = new Ext.util.DelayedTask(function() {
        this.doLayout();
      }, this);
      tempTask.delay(1000);
    }, this);
  },
  
  listenToChildren: function() {
    Ext.each(this.find(), function(field) {
      field.on('change', function() {
        // Check if the form is valid
        var f = this.getForm();
        if (f.isValid()) {
          this.submitButton.enable();
        } else {
          this.submitButton.disable();
        };
      }, this);
    }, this);
  },
  
  createDeleteButton: function() {
    var deleteBtn = this.el.createChild({
      cls: 'x-delete-wrap',
      children: [
        {
          cls: 'x-delete',
          children: [
            {
              cls: 'action-delete'
            }
          ]
        }
      ]
    });
    
    deleteBtn.enableDisplayMode();
    // deleteBtn.anchorTo(this.el.child('.x-panel-header'), 'tr-tr', [17, 0]);
    
    deleteBtn.hide();
    
    this.el.on('mouseover', function() { deleteBtn.show(); deleteBtn.anchorTo(this.el.child('.x-panel-header'), 'tr-tr', [17, 0]); }, this);
    this.el.on('mouseout',  function() { deleteBtn.hide(); }, this);
    deleteBtn.on('click',   this.toggleNewForm, this);
  },
  
	toggleNewForm: function() {
		this.toggleCollapse();
    this.doLayout();
		
		// Get the first item and focus on it
		var field = this.find()[0];
		if (field) {
			var tempTask = new Ext.util.DelayedTask(function() {
			  field.focus();
			}, this);
			tempTask.delay(500);
		};
	},
	
  /**
   * @property formItemConfig
   * @type Object
   * Default config which will be passed to all form items
   */
  formItemConfig: {
    anchor: '-40',
    xtype:  'textfield'
  },
  
  /**
   * @property ignoreFields
   * @type Array
   * An array of fields not to show in the form (defaults to empty)
   */
  ignoreFields: ['id', 'created_at', 'updated_at'],
  
  /**
   * Builds an array of form items for the given model
   * @param {ExtMVC.Model} model The model to build form items for
   * @return {Array} An array of auto-generated form items
   */
  buildItems: function(model) {
    //check to see if FormFields have been created for this model
    //e.g. for a MyApp.models.User model, checks for existence of MyApp.views.users.FormFields
    var formFields;
    
    try {
      if (formFields = eval(String.format("{0}.views.{1}.FormFields", model.namespace.split(".")[0], this.controllerName || model.modelName.pluralize().toLowerCase()))) {
        return formFields;
      };
    } catch(e) {};
    
    //no user defined form fields, generate them automatically
    var items = [];
    
    for (var i=0; i < model.fields.length; i++) {
      var f = model.fields[i];
      
      //add if it's not a field to be ignored
      if (this.ignoreFields.indexOf(f.name) == -1) {
        items.push(Ext.applyIf({
          name:        f.name,
          fieldLabel: (f.name.replace(/_/g, " ")).capitalize()
        }, this.formItemConfig));
      };
    };
    
    return items;
  },
  
  /**
   * Called when the save button is clicked or CTRL + s pressed.  By default this simply fires
   * the associated controller's 'update' action, passing this.getForm() as the sole argument
   */
  onCreate: function() {
    var form = this.getForm();
    
    //tags
    form.items.each(function(item) {
      if (item.name == "tag_list") {
        var val = item.el.dom.value;
        if (!val) return;
        
        item.view.clearSelections();
        item.collapse();
        item.setRawValue('');
        item.fireEvent('newitem', item, val);
      };
    }, this);
    
    // Check if the form is valid
    if (form.isValid()) {
      this.controller.fireAction('create', null, [form]);
      this.doLayout();
    };
  },
  
  /**
   * Called when the cancel button is clicked or ESC pressed.  By default this simply calls Ext.History.back
   */
  onCancel: Ext.emptyFn,
  
  /**
   * Adds a button to the pod new form
   */
  addButton: function(config, handler, scope) {
    var bc = {
      handler:    handler,
      scope:      scope,
      minWidth:   this.minButtonWidth,
      hideParent: true
    };
    
    if (typeof config == "string") {
      bc.text = config;
    } else {
      Ext.apply(bc, config);
    };
    
    Ext.applyIf(config, {
      color: 'green'
    });
    
    var btn = new Kohive.SmallButton(bc);
    btn.ownerCt = this;
    
    if (!this.buttons) {
      this.buttons = [];
    };
    
    this.buttons.push(btn);
    
    return btn;
  },
  
  /**
   * 
   */
  updateTagsField: function(store) {
    if (this.tagsField) this.tagsField.bindStore(store);
  }
});

Ext.reg('kohive_pod_new', Kohive.views.pod.New);

/**
 * @class Kohive.views.comments.New
 * @extends Ext.form.FormPanel
 * Base class for any scaffold form panel (e.g. new and edit forms)
 */
Kohive.views.comments.New = Ext.extend(Ext.form.FormPanel, {
  
  constructor: function(config) {
    Ext.applyIf(config, {
      /**
       * @property referenceObject
       * @type ExtMVC.Model.Base
       * The model object this comments form is currently attached to (e.g. some model instance)
       */
      referenceObject: null
    });
    
    Kohive.views.comments.New.superclass.constructor.call(this, config);
  },

  initComponent: function() {
    /**
     * @property textarea
     * @type Ext.form.TextArea
     * Description
     */
    this.textarea = new Ext.form.TextArea({
      xtype:          'textarea',
      name:           'comment[content]',
      labelSeparator: '',
      height:         50,
      allowBlank:     false,
			grow:           true,
      enableKeyEvents: true,
      listeners: {
        'keydown': {
          fn: function(field, e) {
            if (Ext.EventObject.getKey() == 13) {
              e.stopPropagation();
            }
          },
          scope: this
        }
      }
    });
    
    /**
     * @property addButton
     * @type Ext.Button
     * The button to submit the form
     */
    this.addButton = new Kohive.SmallButton({
      text:     Ext.ux.Translator.translate('add'),
      scope:    this,
			disabled: true,
      handler:  this.onSubmit
    });
    
    Ext.applyIf(this, {
      title:          Ext.ux.Translator.translate('app_new_comment') + ':',
      hidden:         true,
      cls:            'x-comments-new',
      labelSeparator: '',
      labelWidth:     0,
      items:          [this.textarea],
      buttons:        [this.addButton]
    });
		
		// for some reason the buttonAlign is already set as center, so we need to override it..
		this.buttonAlign = 'right';
    
    Kohive.views.comments.New.superclass.initComponent.apply(this, arguments);

		this.on('render', this.listenToChildren, this);
    
    this.initEvents();
    this.initListeners();
  },
  
  /**
   * Sets up the events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event before-submit
       * Fired when the Add button is pressed.  Return false here to cancel the coming 'submit' event
       * @param {ExtMVC.Model.Base} model The model instance to create a comment on
       * @param {String} comment The comment text
       */
      'before-submit',
      
      /**
       * @event submit
       * Fired when the form is submitted
       * @param {ExtMVC.Model.Base} model The model instance to create a comment on
       * @param {String} comment The comment text
       */
      'submit'
    );
  },
  
  /**
   * Sets up listeners to the comments controller
   */
  initListeners: function() {
    Kohive.OS.getController('comments').on({
      scope: this,
			'create' : this.enableForm
    });
  },

	listenToChildren: function() {
    Ext.each(this.find(), function(field) {
      field.on('change', function() {
        // Check if the textarea is valid
        var ta = this.textarea;
        if (ta.isValid()) {
          this.addButton.enable();
        } else {
          this.addButton.disable();
        };
      }, this);
    }, this);
  },
  
  /**
   * Called when the Add button is pressed.  This dispatches to the comments controller
   * with the reference object and the comment itself
   */
  onSubmit: function() {
    var value      = this.textarea.getValue(),
        controller = Kohive.OS.getController('comments');

		// Disable the form
		this.disableForm();

    if (this.fireEvent('before-submit', this.referenceObject, value)) {
      controller.create.call(controller, this.referenceObject, value);
      
      this.fireEvent('submit', this.referenceObject, value);
    }
  },
	
	/**
	 * Method which is called when a new comment is created. It resets the form, enables
	 * it and enabled the add button
	 */
	enableForm: function() {
		this.form.reset();
		this.enable();
		this.addButton.enable();
	},
	
	/**
	 * Called when a new comment is submitted. Disables the form and add button.
	 */
	disableForm: function() {
		this.disable();
		this.addButton.disable();
	},

  /**
   * Sets this form's current model instance to any instance of any model (e.g. a Kohive.models.Image instance)
   * @param {ExtMVC.Model.Base} instance The model instance
   */
  setReferenceObject: function(instance) {
    this.referenceObject = instance;
  }
});

/**
 * @class Kohive.views.comments.Index
 * @extends Ext.Panel
 * Reusable Panel to display comments for a given model
 */
Kohive.views.comments.Index = Ext.extend(Ext.Panel, {
  
  constructor: function(config) {
    config = config || {};
    
    Ext.applyIf(config, {
      /**
       * @property referenceObject
       * @type ExtMVC.Model.Base
       * The reference model instance to show comments for (any MVC Model instance)
       */
      referenceObject: null      
    });
    
    Kohive.views.comments.Index.superclass.constructor.call(this, config);
  },

  initComponent: function() {
    /**
     * @property store
     * @type Ext.data.Store
     * The comments store currently bound to this comments panel.
     * Initially, this is set to a fake empty store and is updated automatically
     * as this component listens to events fired by the comments controller
     */
    this.store = new Ext.data.Store({
      autoLoad: true,
      proxy:    new Ext.data.MemoryProxy({"comment": []}),
      reader:   Kohive.models.Comment.getReader()
    });
    
    /**
     * @property dataview
     * @type Ext.DataView
     * The dataview used to render the comments collection
     */
    this.dataview = new Ext.DataView({
      xtype:        'dataview',
      emptyText:    'No comments found',
      autoHeight:   true,
      autoScroll:   true,
      store:        this.store,
      overClass:    'x-over',
      itemSelector: 'div.x-comment-wrap',
      tpl:          this.template
    });
    
    /**
     * @property controller
     * @type ExtMVC.Controller
     * A reference to the comments controller.
     * This is an unusual thing to do in a view but in this case we don't have much option but
     * to couple to the controller as these views are used all over the place :/
     */
    this.controller = Kohive.OS.getController('comments');
    
    Ext.applyIf(this, {
      hidden: true,
      border: false,
      cls:    'x-comments-wrap',
      title:  'Comments:',
      items:  [this.dataview],
      
      closeAction: 'hide',
      /**
       * @property autoReloadComments
       * @type Boolean
       * True to automatically reload comments when a new reference object is set via
       * setReferenceObject().  Defaults to false
       */
      autoReloadComments: false
    });
    
    Kohive.views.comments.Index.superclass.initComponent.apply(this, arguments);
    
    this.initListeners();
  },
  
  /**
   * Sets this panel's current model instance to show comments for
   * @param {ExtMBV.Model.Base} instance The new model instance
   */
  setReferenceObject: function(instance) {
    if (typeof instance != 'undefined') {
      this.referenceObject = instance;
      if (this.autoReloadComments) {
        this.controller.load(instance);
      }
    }
  },
  
  /**
   * Sets up listeners on the comments controller
   */
  initListeners: function() {
    this.controller.on({
      scope   :  this,
      'load'  : this.onLoad,
      'create': this.onCreate
    });
    
    this.dataview.on({
      scope   : this,
      'click' : this.onClick
    });
  },
  
  /**
   * This is called whenever the comments controller fires its 'load' event.
   * We are only actually interested in events targetting the current model instance, so all
   * other firings are ignored
   * @param {Ext.data.Store} comments The Ext.data.Store containing comments
   * @param {ExtMVC.Model.Base} instance The model instance the comments are for
   */
  onLoad: function(comments, instance) {
    this.ifThisModel(instance, function() {
      this.store = comments;
      this.dataview.setStore(this.store);
      
      if (this.store.getTotalCount() == 0) {
				this.hide();
			} else {
				this.show();
				this.el.hide();
				this.el.fadeIn({duration:0.3});
			};
    });
  },
  
  /**
   * This is called whenever the comments controller fires its 'create' event.
   * We are only actually interested in events targetting the current model instance, so all
   * other firings arr ignored
   * @param {Kohive.models.Comment} comment The newly created comment
   * @param {ExtMVC.Model.Base} instance The model instance the comment was created for
   */
  onCreate: function(comment, instance) {
    this.ifThisModel(instance, function() {
      Kohive.OS.getController('comments').load(instance);
    });
  },
  
  /**
   * 
   */
  onClick: function(dataview, index, node, e) {
    var el = Ext.get(e.getTarget());
    if (!el.hasClass('x-delete')) return;
    
    //get the comment
    var comment = dataview.getRecord(node);
    
    return Ext.Msg.show({
      title:   Ext.ux.Translator.translate(''),
      msg:     Ext.ux.Translator.translate('delete_comment_desc'),
      buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
      scope:   this,
      fn:      function(msg) {
        if (msg == "yes") {
          //destroy the comment
          comment.destroy({
            url:     Kohive.OS.getController('comments').commentUrl(comment),
            scope:   this,
            success: this.onDestroySuccess.createDelegate(this, [comment, node]),
            failure: this.onDestroyFailure.createDelegate(this, [comment, node])
          });
        };
      }
    });
  },
  
  onDestroySuccess: function(comment, node) {
    var el = Ext.get(node);
    el.remove();
    
    this.store.load();
  },
  
  onDestroyFailure: function(comment, node) {
    
  },
  
  /**
   * Calls a function is the current model instance reference matches the one supplied
   * @param {ExtMVC.Model.Base} instance A model instance.  If this is the same as this.referenceObject, callback is called
   * @param {Function} callback The function to call
   */
  ifThisModel: function(instance, callback) {
    if (instance === this.referenceObject) {
      callback.call(this);
    };
  },
  
  /**
   * @property template
   * @type Ext.XTemplate
   * The template for each comment
   */
  template: new Ext.XTemplate(
    '<tpl for=".">',
      '<div class="x-comment-wrap">',
        '<tpl if="can_delete"><div class="x-delete"></div></tpl>',
        '<img src="{thumb_filename}" title="Avatar" class="x-avatar" align="left" />',
        '<div class="x-content">{content}</div>',
        '<div class="x-info">',
          '{[Ext.ux.Translator.translate("posted_by_x_on_x", { nickname: values.author_nickname, date: values.created_at_date })]}',
        '</div>',
      '</div>',
    '</tpl>'
  )
});

/**
 * Kohive.views.hives.Button
 * Customised Button for use in a HivePanel.  Creates a HiveSummary onMouseOver, which is destroy onMouseOut
 * @extends Ext.Button
 */
Kohive.views.hives.Button = function(membership) {
  var config = {};
  
  this.os = Kohive.OS;
  this.controller = this.os.getController('hives');
  
  this.membership = membership;
  this.hive       = membership.data.hive || {};
  
  // Update the hive message + state
  this.hive.message = this.membership.data.message;
  this.hive.state   = this.membership.data.state;
  
  Kohive.views.hives.Button.superclass.constructor.call(this, config);
  
  this.on('render', this.initalizeDropTarget,   this);
  this.on('render', this.initalizeButtonEvents, this);
	
	// Check if the hive is starred
  if (this.membership.data.starred) {
    this.notification = true;
  } else {
    this.notification = false;
  };
};

Ext.extend(Kohive.views.hives.Button, Ext.Button, {
  /**
   * @property toggleGroup
   * @type String
   * The toggle group for the hvie panel. Is used for toggling the hive tabs.
   */
  toggleGroup:      'hives',
  
  /**
   * @property enableToggle
   * @type Boolean
   * True if you want hive buttons to be toggled
   */
  enableToggle:     true,
  
  /**
   * @property allowDepress
   * @type Boolean
   * True if the hive tab can be un-toggled
   */
  allowDepress:     false,
  
  /**
   * @property nameLength
   * @type Integer
   * The length of characters the name of the hive will be before it is
   * concatanted with eclipses (my new nam...)
   */
  nameLength:       20,
  
  /**
   * @property scroller
   * @type Boolean
   * True if the hive scroller is currently active on the browser
   */
  scoller:        false,
  
  /**
   * @property activeDistance
   * @type Integer
   * The amount of pixels the hive tab gos up when activated
   */
  activeDistance: 7,
  
  /**
   * @property template
   * @type Ext.Template
   * The hive tab template
   */
  template: new Ext.Template(
    '<div class="hive {type}">',
			'<div class="notification"></div>',
      '<div class="left">',
        '<div class="right">',
          '<div class="center">{title}</div>',
        '</div>',
      '</div>',
    '</div>'
  ),
  
  /**
   * Attaches on mouse over and on mouse out event listeners for the hive tab.
   * This is used for the hive options to be displayed. It is called after render.
   */
  initalizeButtonEvents: function() {
    // Add the mouseOver event to just the inner .hive elements
    this.buttonEl = this.el.child('.left');
    
    // Add mouseOver and mouseOut events to the button which trigger the
    // hive options to appear
    this.buttonEl.on({
      'mouseover': {scope: this, fn: this.onMouseOverEvent},
      'mouseout':  {scope: this, fn: this.onMouseOutEvent}
    });
  },
  
  // private
  onRender : function(ct) {
    // var type = "standard";
    // if (this.hive.type === 1) type = "free";
    // if (this.hive.type === 2) type = "bronze";
    // if (this.hive.type === 3) type = "silver";
    // if (this.hive.type === 4) type = "gold";
		
    var btn, config = {
      title: Ext.util.Format.ellipsis(this.hive.title, this.nameLength),
      type:  this.hive.type
    };
    
    // Compiling the template onto the hive button
    this.el = this.template.append(ct, config, true);
    
    // Animate the new hive tab if it is new
    if (this.hive.isNew) this.animateNewHive();
    
    // Check if it is a pending hive
    if (this.hive.state == "pending") {
			this.el.addClass('x-pending');
			this.setupPendingBubble(true);
		};
    
    // Position the hive panel now that the hive tab has been added
    ExtMVC.OS.getOS().hivePanel.positionHivePanel();
    
    // Adding onClick event to the button
    this.el.on(this.clickEvent, this.onClickEvent, this);
    
    this.el.on('mouseover', function() { this.el.addClass('x-over'); }, this);
    this.el.on('mouseout', function() { this.el.removeClass('x-over'); }, this);
    
		this.notificationEl = this.el.child('.notification');
		this.notificationEl.enableDisplayMode();
		
    // Register toggle manager
    Ext.ButtonToggleMgr.register(this);
		
		// Check if the icon needs to be displayed on load
    if (!this.notification) this.notificationEl.hide();
		
		if (!Kohive.OS.getCurrentHive()) return;
		if (this.hive.id == Kohive.OS.getCurrentHiveId()) this.notificationEl.hide();
  },
  
  /**
   * This method is used when a user creates a new hive. Animates the new
   * hive tab into position using jQuery.
   */
  animateNewHive: function() {
    // used for jquery function which have no scope, sigh.
    var obj = this;
    
    // get the width + height then hide it
    var htw = this.el.getWidth();
    var hth = this.el.getHeight();
    
    // hide the new hive tab
    jQuery('#' + this.el.id).hide();
    
    // resize the hive panel
    ExtMVC.OS.getOS().hivePanel.positionHivePanel(true, 0);
    
    // Create the faker for the new button. this is used for the opening
    // animation where the space clears before the hive tab appears
    var faker = this.el.parent().createChild({
      id:  'faker-' + this.hive.id,
      cls: 'hive',
      children: [
        {
          tag: 'div'
        }
      ]
    });
    
    // Get the inner faker which we use to set it width
    var fakerInner = faker.select('div');
    fakerInner.setWidth(1);
    fakerInner.setHeight(1);
    
    // Animate the width of the faker to the size of the new hive tab
    jQuery('#' + faker.id).animate({
      width:htw
    }, 300, function() {
      // After the animation is done, remove the faker as we don't need it anymore
      faker.remove();
      
      // Show the new hive tab and set its top position to the height of the tab, so
      // it is 'below' the browser window.
      jQuery('#' + obj.el.id).show();
      jQuery('#' + obj.el.id).css('top', hth);
      
      if (ExtMVC.OS.getOS().hivePanel.scroller) {
        ExtMVC.OS.getOS().hivePanel.scroll(0, true, false, function() {
          jQuery('#' + obj.el.id).animate({
            top:0
          }, 300, function() {
            // Get the OS and the controller
            var os         = ExtMVC.OS.getOS();
            var controller = os.getController('hives');
      
            // Refresh the hive panel
            // controller.fireAction('refresh');
            
            obj.onClickEvent();
          });
        }, this);
      } else {
        jQuery('#' + obj.el.id).animate({
          top:0
        }, 300, function() {
          // Get the OS and the controller
          var os         = ExtMVC.OS.getOS();
          var controller = os.getController('hives');
          
          // Refresh the hive panel
          // controller.fireAction('refresh');
          
          obj.onClickEvent();
        });
      };
    });
  },
  
  /**
   * This method is used when the user leaves a hive or cancels a new hive inivitation
   */
  animateRemoveHive: function() {
    // used for jquery function which have no scope, sigh.
    var obj = this;
    
    // get the width + height then hide it
    var htw = this.el.getWidth();
    var hth = this.el.getHeight();
    
    jQuery('#' + obj.el.id).animate({
      top: hth
    }, 300, function() {
      // Hide the hive tab
      obj.destroy();
      
      ExtMVC.OS.getOS().hivePanel.positionHivePanel(true, 0);
      
      // Get the OS and the controller
      var os         = ExtMVC.OS.getOS();
      var controller = os.getController('hives');
      
      // Refresh the hive panel
      // controller.fireAction('refresh');
    });
  },
  
  /**
   * Override the default onClick event so we can choose which elements
   * we want to attach the onClick event too.
   * @param {Event} e
   */
  onClickEvent: function(e) {
    // Checks if the element clicked is the hive options bar
    if (e) var isOptionBtn = e.getTarget('span');
    if (isOptionBtn) return;
    
    if (e) e.preventDefault();
    
    var os          = ExtMVC.OS.getOS();
    var currentHive = os.getCurrentHive() || null;
    
		// Check if it is a pending hive
    if (this.hive.state == "pending") return this.setupPendingBubble();
    
    // Check if the hive is already loaded
    if (currentHive && currentHive.data.hive.id == this.hive.id) return;
    
    // If the button isn't disabled, continue
    if (!this.disabled) {
      // Fire off events
      this.fireEvent("click", this, this.hive.id);
      
      var controller = Kohive.OS.getController('hives');
			controller.fireEvent('beforehiveload', controller, this);
      controller.fireAction('loadHive', controller, [null, this, true]);
      
      //create the invite dialog, and display it in the middle of the screen
      if (this.hive.isNew) this.createInviteDialog();
    };
  },
  
  /**
   * 
   */
  createInviteDialog: function() {
    // var tempTask = new Ext.util.DelayedTask(function() {
    //   var body   = Ext.getBody(),
    //       dialog = new Kohive.views.hives.Invite();
    //   
    //   dialog.render(body);
    //   dialog.el.anchorTo(body, 'c-c');
    //   dialog.el.hide();
    //   dialog.el.fadeIn();
    // }, this);
    // tempTask.delay(2000);
  },
  
  /**
   * Toggles the hive tab
   */
  toggle: function(isNew) {
    var hp = jQuery('#hive-panel');
    
    // Check if it is a number or string
    var id = this.el.id;
    
    // Find the tab
    var ht = hp.find('#' + id);
    
    // Check if the tab is already active
    if (ht.hasClass('active')) { return; };
    
    // Find the currently active tab (if there is one) and de-activate it
    var aht = hp.find('.hive.active');
    if (aht) {
      if (!isNew) {
        aht.animate({
          top:"+=" + this.activeDistance
        }, 300);
      } else {
        this.el.applyStyles('top:7px');
        // aht.css('top', this.activeDistance);
      };
      aht.removeClass('active');
    };
    
    if (!isNew) {
      // Animate the hive tab and add a class to it
      ht.animate({
        top:"-=" + this.activeDistance
      }, 300);
    } else {
      this.el.applyStyles('top:-7px');
      // ht.css('top', -this.active);
    };
    ht.addClass('active');
  },
  
  /**
   * Shows the hive options bar and aligns it
   */
  onMouseOverEvent: function() {
    // Show the hive options
    this.os.getHiveOptions().showOptions(this);
    
    // Move the options bar into the hive button and position it
    var hiveOptionsEl = this.os.getHiveOptions().el;
    var insertEl      = this.el.child('.center');
    insertEl.insertFirst(hiveOptionsEl);
  },
  
  /**
   * Hides the global HiveOptions object if it is visible
   */
  onMouseOutEvent: function(){
    this.os.getHiveOptions().hideOptions();
  },

  /**
   * Adds a droptarget to the hivebutton elemenet
   * @see memberButton
   */
  initalizeDropTarget: function() {
		var btn = this;
    var hive = this.hive;
    
    // Add a droptarget to the hive
    this.el.dt = new Ext.dd.DropTarget(this.el, {
      ddGroup: 'memberPanel',
      
      /**
       * Called when the user drops a dragsource is 
       */
      notifyDrop: function(ds, e) {
        e.stopEvent();
        
        this.os = Kohive.OS;
        
        // Get the member button form the DS arguement
        var member = ds.member;
				
				//check if you can create the member..
        // var max = (this.hive.max_members == 0) ? 999 : this.hive.max_members;
        // var maxedMembers = Kohive.OS.getCurrentHiveMemberships().length >= max;
        // if (maxedMembers) return;
				
				//add/remove the add class
				btn.addClass('add');
        
        // Check if to see if the user is dragging a member of the currently
        // selected hive into the same selected hive
        if (hive.id == this.os.getCurrentHive().get('id')) return;
        
        // The hive is invite-only so we must fire off an invite
        // if (hive.type == 'InviteOnlyHive') {
          var controller = this.os.getController('members');
          if (controller) controller.fireAction('inviteMember', controller, [member, hive, btn]);
					
					//remove the drag el
					Ext.get(ds.getDragEl()).remove();
        // };
      },
			
 			notifyEnter: function(ds, e) {
				// var el = ds.btn.el.child('.x-member-avatar-add');
				// if (el) { el.remove(); };
				// 
				// ds.btn.el.child('.x-member-avatar').createChild({
				// 	cls: 'x-member-avatar-add'
				// });
				var el = ds.el.child('.x-member-avatar-add');
				if (el) { el.remove(); };
				
				ds.el.child('.x-member-avatar').createChild({
					cls: 'x-member-avatar-add'
				});
 			},

			notifyOut: function(ds, e) {
				var el = ds.el.child('.x-member-avatar-add');
				if (el) { el.remove(); };
 			}
      // 			
      // 			notifyOut: function() {
      // 				Ext.dd.DropTarget.superclass.notifyOut.call(this);
      // 				
      // 				btn.removeClass('add');
      // 			}
    });
  },
  
  /**
   * Shows the notification icon on the hive button. Defaults to false.
   */
  showNotification: function(){
    this.notificationEl.fadeIn({ duration: 0.5 });
    this.notification = true;
  },
  
  /**
   * Hides the notification icon on the hive button.
   */
  hideNotification: function(){
    this.notificationEl.fadeOut({ duration: 0.5 });
    this.notification = false;
  },
  
  /**
   * Toggles the notification icon on the hive button.
   */
  toggleNotification: function(){
    if (this.notification) {
      this.hideNotification();
    } else {
      this.showNotification();
    };
  },
	
	setupPendingBubble: function(first) {
		if (this.bubble) return;
		if (Kohive.OS.getHivePanel().bubbleAlive) return;
		
		var btn = this;
		
		Kohive.OS.getHivePanel().bubbleAlive = true;
		
		this.bubble = new Ext.ux.HUD({
			anchorToEl:  this.el,
      html:        Ext.ux.Translator.translate('pending_hive_bubble_desc') + '<br />',
      header:      false,
      width:       'auto',
      cls:         'add-hive coming-soon',
			pokerHeight: 44,
      clickListener: function(e) {
        if (!e.getTarget('.x-hud-wrapper')) {
					this.destroy();
					btn.bubble = null;
					Kohive.OS.getHivePanel().bubbleAlive = false;
				};
      }
		});
		
		this.bubble.render();
    this.bubble.hide();
		
		//add accept + decline buttons
		var body = this.bubble.el.child('.x-hud-body');
		
		var accept = new Kohive.SmallButton({
			renderTo: body.dom.id,
			color:    'green',
			text:     Ext.ux.Translator.translate('accept')
		});
		var decline = new Kohive.SmallButton({
			renderTo: body.dom.id,
			color:    'red',
			text:     Ext.ux.Translator.translate('decline')
		});
		
		body.applyStyles('max-width:300px;text-align:center;');
		accept.el.applyStyles('display:inline-block;text-align:center;margin-left:10px;margin-top:10px;');
		decline.el.applyStyles('display:inline-block;text-align:center;margin-left:5px;margin-top:10px;');
		
		accept.focus();
		
		accept.on('click', function() {
			var os = Kohive.OS;
			os.hiveOptions.fireEvent('accept', null, this.membership);
			this.bubble.el.fadeOut({
				scope:    this,
				callback: function() {
					this.bubble.destroy();
					this.bubble = null;
					Kohive.OS.getHivePanel().bubbleAlive = false;
				}
			});
		}, this);
		
		decline.on('click', function() {
			var os = Kohive.OS;
			os.hiveOptions.fireEvent('destroy', null, this.membership, this);
			this.bubble.el.fadeOut({
				scope:    this,
				callback: function() {
					this.bubble.destroy();
					this.bubble = null;
					Kohive.OS.getHivePanel().bubbleAlive = false;
				}
			});
		}, this);
    
    var bubble = this.bubble;
    this.bubble.showHUD([0, -50], 'b-t', (first === true) ? 1000 : 1, function() {
      bubble.positionPoker([0, 0], 't-b');
    }, true);
	}
});
Ext.reg('kohive_hive_button', Kohive.views.hives.Button);

/**
 * @class Kohive.views.hives.Index
 * @extends Ext.BoxComponent
 * Manages Hive Buttons
 */
Kohive.views.hives.Index = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    id: 'hivePanel',
    defaults: {
      xtype: 'kohive_hive_button'
    }
  });
  
  Kohive.views.hives.Index.superclass.constructor.call(this, config);
  
  this.addEvents(
    /**
     * @event addhive
     * Fires when you want to add a new hive.
     * @param {Kohive.views.hives.Index} this
     */
    'addhive',
    
    /**
     * @event invite
     * Fires when a new member wants to join the hive. Is used by
     * HiveButton and MemberButton
     * @param {Kohive.MemberButton} btn Instance of the member btn object
     */
    'invite',
    
    /**
     * @event showview
     * Fires when the hivepanel needs to be shown
     */
    'showview'
  );
  
  // Add events to the panel
  this.on('add',      this.doLayout, this);
  this.on('showview', this.showView, this);
  
  this.render();
};

Ext.extend(Kohive.views.hives.Index, Ext.Container, {
  border:       false,
  frame:        false,
  scrollAmount: 150,
  
  /**
   * Creates the required DOM elements for the Hive Panel.
   */
  onRender: function(ct, position) {
    this.el = ct.createChild({
      id:  'hive-panel-wrapper',
      cls: 'x-hive-panel-wrapper',
      children: [
        {
          id:  'hive-panel',
          cls: 'x-hive-panel',
          children: [
            {
              tag: 'div',
              children: [
                {
                  id:  'new-hive',
                  cls: 'New',
                  children: [
                    {
                      cls: 'left',
                      children: [
                        {
                          cls: 'right',
                          children: [
                            {
                              cls:  'center',
                              html: '+'
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        },
        {
          id:  'new-hive-wrapper',
          cls: 'x-new-hive-wrapper'
        },
        {
          id:  'hive-scroller-left',
          cls: 'x-scroller-left',
          hidden: true
        },
        {
          id:  'hive-scroller-right',
          cls: 'x-scroller-right',
          hidden: true
        }
      ]
    });
    
    var backdrop = Ext.getBody().createChild({
      cls: 'x-hive-panel-backdrop'
    });
    
    this.buttons       = this.el.child('.x-hive-panel div');    
    this.newButton     = this.el.child('.New');
    this.leftScroller  = this.el.child('.x-scroller-left');
    this.rightScroller = this.el.child('.x-scroller-right');
    
    // Add click events
    this.newButton.on('click', function() { this.fireEvent('new', this.newButton); }, this);
    this.leftScroller.on('click', function() { this.scrollLeft(); }, this);
    this.rightScroller.on('click', function() { this.scrollRight(); }, this);
    
    // Add over state to the new hive button
    this.newButton.on('mouseover', function() { this.newButton.addClass('x-over'); }, this);
    this.newButton.on('mouseout', function() { this.newButton.removeClass('x-over'); }, this);
    
    this.leftScroller.unselectable();
    this.rightScroller.unselectable();
    
    this.el.hide();
    
    Kohive.views.hives.Index.superclass.onRender.apply(this, arguments);
  },
  
  getLayoutTarget: function() {
    return this.buttons;
  },
  
  /**
   * Fades the view in after a delay of 1 second
   */
  showView: function() {
    this.el.fadeIn({
      duration:1
    });
    
    var obj = this;
    
    jQuery(window).bind('resize', function() {
      obj.positionHivePanel();
    });
  },
  
  /**
   * Used to position the hive panel depending on its contents.
   * This should probably be in the index but i will move it later
   * @param {Boolean} animate Whether to animate this or not. defaults to false
   * @param {Integer} extra Extra width when adding a tab
   */
  positionHivePanel: function(animate, extra) {
    // Hack for jQuery as it has no scope
    var obj = this;
    
    // Get the hive panel
    var hp = jQuery('#hive-panel');
    var extra = extra || 0;
    
    // Get the window and hive panel width
    var ww  = jQuery(window).width();
    var hpw = 0;
    var diff = 0;
    
    var hives = hp.find('.hive');
    hives.each(function() {
      var w = jQuery(this).width();
      
      hpw = hpw + w;
    });
    
    hpl = Math.round((ww / 2) - (hpw / 2));
    hpnw = Math.round(hpw + extra + 5);
    
    var first = false;
    
    var nb = jQuery('#new-hive');
		
    // check if the width is more than the window
    if ((hpnw + 40) > ww) {
      this.scroller = true;
      
      var nbw = jQuery('#new-hive').width();
      
      hpnw = ww - nbw - 39;
      hpl  = 20;
      
      // move the new hive button
      nb.appendTo('#new-hive-wrapper');
    } else {
      this.scroller = false;
      var first = true;
      
      nb.appendTo('#hive-panel > div');
    };
    
    if (first) {
      var nb = jQuery('#new-hive');
      var nbw = jQuery('#new-hive').width();
      
      hpnw = hpnw + nbw + 100;
      hpl  = hpl - (nbw / 2);
    };
    
    // Animate the hive panel into position
    if (animate) {
      // Decide whether to show the scrollers or not
      if (this.scroller) {
        jQuery('#hive-scroller-left').fadeIn();
        jQuery('#hive-scroller-right').fadeIn();
      } else {
        jQuery('#hive-scroller-left').fadeOut();
        jQuery('#hive-scroller-right').fadeOut();
      };
      
      // Animate
      hp.animate({
        left:  hpl,
        width: hpnw
      }, 300, function() {
        obj.scroll(0);
      });
      
      return;
    };
    
    hp.css('left', hpl);
    hp.css('width', hpnw);
    
    // Decide whether to show the scrollers or not
    if (this.scroller) {
      jQuery('#hive-scroller-left').show();
      jQuery('#hive-scroller-right').show();
    } else {
      jQuery('#hive-scroller-left').hide();
      jQuery('#hive-scroller-right').hide();
    };
    
    this.scroll(0);
  },
  
  /**
   * Scrolls the scroller left
   */
  scrollLeft: function() {
    if (!this.scroller) {
      return;
    };
    
    this.scroll(this.scrollAmount);
  },
  
  /**
   * Scrolls the scroller right
   */
  scrollRight: function() {
    if (!this.scroller) {
      return;
    };
    
    this.scroll(-this.scrollAmount);
  },
  
  getScrollAmount: function() {
    var hps = jQuery('#hive-panel > div');
    
    return hps.position().left;
  },
  
  moveToOldPosition: function() {
    
  },
  
  /**
   * Scrolls the scroller, k
   * @param {Integer} scrollAmount The amount you want to scroll
   */
  scroll: function(scrollAmount, end, dontAnimate, callback, scope) {
    var hp  = jQuery('#hive-panel');
    var hps = jQuery('#hive-panel > div');
    
    // Get the width of all hive tabs
    var hpsw = 0;
    var hives = hp.find('.hive');
    hives.each(function() {
      var w = jQuery(this).width();
      hpsw = hpsw + w;
    });
    
    // get the width of the hive panel
    var hpw = hp.width();
    
    // get the difference between the size of the scroller and the window
    var diff = hpsw - hpw;
    diff = -diff;
    
    // if the end is set, then goto the diff (end)
    if (end) {
      nl = diff;
    } else {
      // get the current left position
      var hpsl = hps.position().left;
      
      // calculate the new left position
      var nl = hpsl + scrollAmount;
      
      // check the boundaries
      if (nl < diff) {
        nl = diff;
        this.rightScroller.addClass('hidden');
        this.leftScroller.removeClass('hidden');
      };
      if (nl >= 0) {
        nl = 0;
        this.rightScroller.removeClass('hidden');
        this.leftScroller.addClass('hidden');
      };
    };
    
    // // Scroll it tbh
    if (dontAnimate) {
      var hivePanel = Ext.getBody().child('#hive-panel > div');
      hivePanel.setLeft(nl);
      // console.log(hivePanel.getLeft());
      
      if (callback) { callback.call(scope || this); };
    } else {
      hps.animate({
        left: nl
      }, 200, function() {
        if (callback) { callback.call(scope || this); };
      });
    };
    
    this.latest = nl;
  }
});

/**
 * Kohive.views.hives.Invite
 * @extends Ext.ux.HUD
 * A floating HUD which shows when a new hive is created
 */
Kohive.views.hives.Invite = function(config) {
  var config = config || {};
  
  //get the current hive so we can use it for translations
  var hive = Kohive.OS.getCurrentHive().get('hive');
  
  var twitter  = new Kohive.IconButton({iconCls:'twitter', handler:this.onTwitter, scope:this}),
      facebook = new Kohive.IconButton({iconCls:'facebook', handler:this.onFacebook, scope:this});
  
  this.message = Ext.ux.Translator.translate('hive_invite_dialog_message_body', { title: hive.title, code: hive.invite_hashcode });
  
  Ext.applyIf(config, {
    title:  '',
    header: false,
    poker:  false,
    
		width: 420,
		
    cls: 'add-hive-invite',
    id:  'addHivePanel',
    
    items: [
      {
        cls:  'x-desc',
        html: Ext.ux.Translator.translate('hive_invite_dialog_desc', { code: hive.invite_hashcode })
      },
      {
        html: Ext.ux.Translator.translate('hive_invite_dialog_message_title')
      },
      {
        cls:  'x-message',
        html: this.message
      },
      // {
      //   bodyStyle: 'margin-top:5px;background:transparent;',
      //   layout:    'column',
      //   defaults: {
      //     border:    false,
      //     bodyStyle: 'background:transparent'
      //   },
      //   items: [
      //     {
      //       width:     163,
      //       bodyStyle: 'background:transparent;text-align:right',
      //       items:     [twitter]
      //     },
      //     {
      //       items: [facebook]
      //     }
      //   ]
      // }
      {
        bodyStyle: 'background:transparent;text-align:center;',
        defaults: {
          border:    false,
          bodyStyle: 'background:transparent'
        },
        items: [twitter]
      }
    ]
  });
 
  Kohive.views.hives.Invite.superclass.constructor.call(this, config);
  
  this.on('render', this.initCloseButton, this);
  this.on('render', this.initBodyListener, this);
};

Ext.extend(Kohive.views.hives.Invite, Ext.ux.HUD, {
  /**
   * 
   */
  initCloseButton: function() {
    this.closeBtn = new Kohive.IconButton({
      iconCls: 'close',
      scope:   this,
      handler: this.onClose
    });
    
    var tempTask = new Ext.util.DelayedTask(function() {
      this.closeBtn.render(this.el);
      this.closeBtn.el.anchorTo(this.el, 'tr', [-8, -6]);
      this.closeBtn.el.hide();
      this.closeBtn.el.fadeIn();
    }, this);
    tempTask.delay(1000);
  },
  
  /**
   * 
   */
  initBodyListener: function() {
    var body = Ext.getBody();
    
    body.on('click', function(e) {
      var el       = Ext.get(e.getTarget()),
          hasClass = false;
      
      while (!hasClass) {
        if (el.hasClass('x-hud-wrapper')) return;
        
        var el = el.parent();
        if (!el) {
          hasClass = true;
          this.onClose();
        };
      };
    }, this);
  },
  
  /**
   * 
   */
  onClose: function() {
    this.el.fadeOut({
      scope: this,
      callback: function() {
        this.destroy();
      }
    });
  },
  
  /**
   * Opens the browser at twitter.com with the message in the status field
   */
	onTwitter: function() {
	  var message = escape(this.message),
	      url     = 'http://twitter.com/home?status=' + message;
	  
    window.open(url);
	}
});
Ext.reg('kohive_hive_invite', Kohive.views.hives.Invite);

Kohive.views.hives.New = Ext.extend(Ext.form.HUD, {
  panesSetup: false,
  
	constructor: function(config) {
		var config = config || {};

	  this.saveButton = this.getSaveButton();

	  this.anchorToEl = Kohive.OS.hivePanel.newButton;

	  Ext.applyIf(config, {
	    title:       Ext.ux.Translator.translate('new_hive'),
	    cls:         'add-hive',
	    id:          'addHivePanel',
	    pokerHeight: 44,
	    items: [
				{
					id: 'new_hive_field',
	        layout: 'column',
	        style:  'margin:6px 0 12px 0;',
	        defaults: {
	          layout: 'form',
	          border: false,
	          xtype:  'panel'
	        },
	        items: [
	          {
	            columnWidth: .3,
	            html:        Ext.ux.Translator.translate('name') + ':',
	            bodyStyle:   'background:transparent;margin:0px 5px 0 14px;padding-top:5px;',
	            border:      false
	          },
	          {
	            columnWidth: .6,
	            xtype: 'textfield',
	            id:    'new_hive_title',
	            name:  'title',
	            allowBlank: false
	          },
	          {
	            columnWidth: .2,
	            bodyStyle:   'background:transparent;margin-left:5px;padding-top:2px',
	            border:      false,
	            items: [
	              this.saveButton
	            ]
	          }
	        ]
	      },
				{
					id:    'new_hive_message',
	        style: 'margin:10px 15px 15px 15px;text-align:center;',
					html:  Ext.ux.Translator.translate('new_pro_hive_error')
	      },
	      {
  				id:     'new_hive_message_buttons',
  	      layout: 'column',
  	      style:  'margin:4px 0 15px 38px;',
  	      items: [
  	        new Kohive.SmallButton({
              text:     Ext.ux.Translator.translate('view_plans'),
              scope:    this,
              style:    'margin-right:5px',
              handler:  this.openSettingsPane
            }),
            new Kohive.SmallButton({
              text:     Ext.ux.Translator.translate('cancel'),
              color:    'red',
              scope:    this,
              handler:  this.destroy
            })
  	      ]
  	    }
	    ],

	    //esc key cancels, enter submits
	    keys: [
	      {
	        key: Ext.EventObject.ENTER,
	        fn:  this.fireEvent.createDelegate(this, ['save', this])
	      },
	      {
	        key: Ext.EventObject.ESC,
	        fn:  this.fireEvent.createDelegate(this, ['cancel', this])
	      }
	    ]
	  });

		Kohive.views.hives.New.superclass.constructor.call(this, config);
		
		this.initListeners();
	},
	
	/**
	 * 
	 */
	initListeners: function() {
	  this.on('render', this.listenToFieldChange, this);
	  this.on('show',   this.initTypeEvents, this);
	  this.on('show',   this.setupPanes, this);
	},
	
	/**
	 * 
	 */
	setupPanes: function() {
		if (this.panesSetup) return;
		
		this.panesSetup = true;
		
		Ext.get('new_hive_field').enableDisplayMode();
		Ext.get('new_hive_message').enableDisplayMode();
		Ext.get('new_hive_message_buttons').enableDisplayMode();
		Ext.get('new_hive_message').hide();
		Ext.get('new_hive_message_buttons').hide();
		
		var user = Kohive.OS.getCurrentUser();
    if (user && !user.canCreateProHive()) this.showMessage();
	},
	
	/**
	 * 
	 */
	showMessage: function(message) {
	  var field      = Ext.get('new_hive_field'),
	      msg        = Ext.get('new_hive_message'),
	      msgButtons = Ext.get('new_hive_message_buttons');
		
		//update the text of the message pane
		if (message) msg.update(message);
		
		var before = this.el.child('.x-hud-body').getHeight();
		
		if (field.isVisible()) {
			field.hide();
			msg.show();
			msgButtons.show();
			
			before = before - 7;
		} else {
			msg.hide();
			msgButtons.hide();
			field.show();
			
			before = before + 7;
		};
		
		var after = this.el.child('.x-hud-body').getHeight();
		var diffA = before - after;
		var diffB = after - before;
		
		this.el.setTop(this.el.getTop() + diffA);
		this.el.setHeight('auto');
	},
	
  /**
   * Adds onClick events to each of the type buttons
   */
  initTypeEvents: function() {
		Ext.getCmp('new_hive_title').reset();
		
		
    var tempTask = new Ext.util.DelayedTask(function() {
      Ext.get('new_hive_title').focus();
    }, this);
    tempTask.delay(100);
  },
  
  /**
   * Listens to the new hive title field for changes
   */
	listenToFieldChange: function() {
		Ext.getCmp('new_hive_title').on('change', function(item) {
			if (!item.isValid()) return this.saveButton.disable();
			return this.saveButton.enable();
		}, this);
	},
	
	/**
	 * opens the settings view and opens the accounts view... also destroys the hud
	 */
	openSettingsPane: function() {
	  var controller = Kohive.OS.getController('settings');
	  if (controller) controller.index('settings_view_personal_account');
	  this.destroy();
	},
	
	/**
	 * 
	 */
	getSaveButton: function() {
	  return new Kohive.IconButton({
	    iconCls:  'add',
	    scope:    this,
			disabled: true,
	    handler:  this.fireEvent.createDelegate(this, ['save', this])
	  });
	},
	
	/**
   * Marks the appropriate errors on the panel
   * @param {Kohive.models.Hive} hive The hive model instance containing the errors 
   */
  onFailure: function(hive, data) {
    //check if the error is level related
    if (data.errors[0][0] == "level") this.showMessage(Ext.ux.Translator.translate('too_many_hives'));
    else this.showMessage(Ext.ux.Translator.translate('something_wrong'));
  }
});
Ext.reg('kohive_hive_new', Kohive.views.hives.New);

/**
 * Kohive.views.hives.New
 * @extends Ext.form.HUD
 * A floating FormPanel containing a form to add a new Hive
 */
Kohive.views.hives.NewIntro = function(config) {
  var config = config || {};
	
	//set firstHive to true
	this.firstHive = true;
	
  this.saveButton1 = new Kohive.IconButton({
    iconCls:  'add',
    scope:    this,
		disabled: true,
    handler:  this.onSave
  });
  
  this.saveButton2 = new Kohive.IconButton({
    iconCls:  'add',
    scope:    this,
		disabled: true,
    handler:  this.onSave
  });
  
  // Create a new hidden field for the hive type
  this.hiddenField = new Ext.form.Hidden({
    name:      'type',
    hideLabel: true,
    value:     'PersonalHive',
    id:        'add-hive-type'
  });
  
  this.anchorToEl = Kohive.OS.hivePanel.newButton;
  
  Ext.applyIf(config, {
		width:       260,
    title:       Ext.ux.Translator.translate('new_hive'),
    cls:         'add-hive',
    id:          'addHivePanel',
    pokerHeight: 44,
    items: [
      {
  		  id:        'hive_intro_section_1a',
        bodyStyle: 'padding:0 10px 0 10px;background:transparent;border:0;color:#ccc;',
  			html:      '<center>' + Ext.ux.Translator.translate('create_first_hive') + '</center>'
  		},
      {
        id:     'hive_intro_section_1b',
        layout: 'column',
        style:  'margin:10px 0 12px 12px;',
        defaults: {
          layout: 'form',
          border: false,
          xtype:  'panel'
        },
        items: [
          {
            columnWidth: .8,
            xtype:       'textfield',
            bodyStyle:   'margin:5px 0;',
						width:       205,
            id:          'intro_hive_title1',
            name:        'title',
            allowBlank:  false
          },
          {
            columnWidth: .2,
            bodyStyle:   'background:transparent;margin-left:5px;padding-top:2px',
            border:      false,
            items: [
              this.saveButton1
            ]
          }
        ]
      },
			{
			  id:        'hive_intro_section_1c',
        bodyStyle: 'padding:0 10px 10px 10px;background:transparent;border:0;color:#ccc;',
				html:      '<center>' + Ext.ux.Translator.translate('create_first_hive_desc') + '</center>'
			},
      // {
      //        id:        'hive_intro_section_2a',
      //         bodyStyle: 'padding:0 10px 0 10px;background:transparent;border:0;color:#ccc;',
      //        html:      '<center>' + Ext.ux.Translator.translate('create_first_hive_pro') + '</center>'
      //      },
      {
        id:     'hive_intro_section_2b',
        layout: 'column',
        style:  'margin:10px 0 12px 12px;',
        defaults: {
          layout: 'form',
          border: false,
          xtype:  'panel'
        },
        items: [
          {
            columnWidth: .8,
            xtype:       'textfield',
            bodyStyle:   'margin:5px 0;',
						width:       205,
            id:          'intro_hive_title2',
            name:        'title',
            allowBlank:  false
          },
          {
            columnWidth: .2,
            bodyStyle:   'background:transparent;margin-left:5px;padding-top:2px',
            border:      false,
            items: [
              this.saveButton2
            ]
          }
        ]
      },
			{
			  id:        'hive_intro_section_2c',
        bodyStyle: 'padding:0 10px 10px 10px;background:transparent;border:0;color:#ccc;',
				html:      '<center>' + Ext.ux.Translator.translate('create_first_hive_desc_pro') + '</center>'
			},
      this.hiddenField
    ],
    
    //esc key cancels, enter submits
    keys: [
      {
        key:   Ext.EventObject.ENTER,
				scope: this,
        fn:    this.onSave
      }
    ]
  });
 
  Kohive.views.hives.NewIntro.superclass.constructor.call(this, config);
  
	this.on('render', this.listenToFieldChange, this);
  this.on('show',   this.initTypeEvents, this);
  this.on('show',   this.setupTabs, this);
};

Ext.extend(Kohive.views.hives.NewIntro, Ext.form.HUD, {
	onSave: function() {
		this.fireEvent('save', this);
	},
	
  /**
   * Adds onClick events to each of the type buttons
   */
  initTypeEvents: function() {
		this.find()[2].reset();
		
    // Set default onLoad type
    this.changeType('InviteOnlyHive');
    // this.inviteOnlyEl.addClass('pressed');
    
    var tempTask = new Ext.util.DelayedTask(function() {
      Ext.get('intro_hive_title1').focus();
    }, this);
    tempTask.delay(100);
  },
	
	tabsSetup: false,
	tab:       2,
	
	setupTabs: function() {
		if (this.tabsSetup) return;
		
		this.tabsSetup = true;
		
		Ext.get('hive_intro_section_1a').enableDisplayMode();
		Ext.get('hive_intro_section_1b').enableDisplayMode();
		Ext.get('hive_intro_section_1c').enableDisplayMode();
    // Ext.get('hive_intro_section_2a').enableDisplayMode();
		Ext.get('hive_intro_section_2b').enableDisplayMode();
		Ext.get('hive_intro_section_2c').enableDisplayMode();
    // Ext.get('hive_intro_section_2a').hide();
		Ext.get('hive_intro_section_2b').hide();
		Ext.get('hive_intro_section_2c').hide();
		
		// if (!this.firstHive) Ext.get('new_member_email_text').hide();
		
    // var btnA = this.el.child('.x-hud-header-text');
    // btnA.dom.id = "intro_hive_goto_1";
    //     // btnA.addClass('x-active');
    // 
    // var headerEl = this.el.child('.x-hud-header');
    // var btnB = headerEl.createChild({
    //  id:   'intro_hive_goto_2',
    //  tag:  'span',
    //  cls:  'x-hud-header-text x-active',
    //  html: Ext.ux.Translator.translate('pro_applications')
    // });
    // 
    //     btnA.on('click', this.switchToStandard, this);
    //     btnB.on('click', this.switchToPro, this);
	},
	
	/**
	 * 
	 */
	switchToStandard: function() {
		if (this.tab === 1) return;
		this.tab = 1;
		
    var before = (this.textLast) ? this.el.child('.x-hud-body').getHeight() + 7 : this.el.child('.x-hud-body').getHeight();
		
    // this.questionMarkEl.hide();
		
		Ext.get('hive_intro_section_1a').show();
		Ext.get('hive_intro_section_1b').show();
		Ext.get('hive_intro_section_1c').show();
    // Ext.get('hive_intro_section_2a').hide();
		Ext.get('hive_intro_section_2b').hide();
		Ext.get('hive_intro_section_2c').hide();
		
		Ext.get('intro_hive_goto_1').addClass('x-active');
		Ext.get('intro_hive_goto_2').removeClass('x-active');
		
		var after = this.el.child('.x-hud-body').getHeight();
		var diffA = before - after;
		var diffB = after - before;

		this.el.setTop(this.el.getTop() + diffA);
		this.el.setHeight(this.el.getHeight() + diffB);
		
    Ext.getCmp('intro_hive_title1').focus();
    Ext.getCmp('intro_hive_title1').clearInvalid();
	},
	
	/**
	 * 
	 */
	switchToPro: function() {
		if (this.tab === 2) return;
		this.tab = 2;
		
		var before = this.el.child('.x-hud-body').getHeight();
		
		Ext.get('hive_intro_section_1a').hide();
		Ext.get('hive_intro_section_1b').hide();
		Ext.get('hive_intro_section_1c').hide();
    // Ext.get('hive_intro_section_2a').show();
		Ext.get('hive_intro_section_2b').show();
		Ext.get('hive_intro_section_2c').show();
		
		Ext.get('intro_hive_goto_1').removeClass('x-active');
		Ext.get('intro_hive_goto_2').addClass('x-active');
		
		var after = this.el.child('.x-hud-body').getHeight();
		var diffA = before - after;
		var diffB = after - before;

		this.el.setTop(this.el.getTop() + diffA);
		this.el.setHeight(this.el.getHeight() + diffB);
		
    Ext.getCmp('intro_hive_title2').focus();
    Ext.getCmp('intro_hive_title2').clearInvalid();
	},
	
	getValue: function() {
	  return Ext.getCmp('intro_hive_title1').getValue();
	},
	
	getLevel: function() {
	  return (this.tab == 1) ? 0 : 1;
	},
	
	addClickListenerToBody: function() {},
  
  /**
   * Changes the type in the hidden input field
   * @param {String} type The type of hive
   */
  changeType: function(type) {
    if (!type) return;
		
    this.hiddenField.el.dom.value = type;
  },

	listenToFieldChange: function() {
		// Listen for keypress events on the new member email field
		this.find()[2].on('change', function(item) { return (item.isValid()) ? this.saveButton1.enable() : this.saveButton1.disable(); }, this);
		this.find()[8].on('change', function(item) { return (item.isValid()) ? this.saveButton2.enable() : this.saveButton2.disable(); }, this);
	}
});
Ext.reg('kohive_hive_new_intro', Kohive.views.hives.NewIntro);

/**
 * Kohive.views.hives.Options
 * @extends Ext.Panel
 * Provides a bar on hive rollover with options within it
 */
Kohive.views.hives.Options = function(config) {
  var config = config || {};
    
  Ext.applyIf(config, {
    hidden: true,
    id:     'hive-options',
    cls:    'x-hive-options'
  });
    
  Kohive.views.hives.Options.superclass.constructor.call(this, config);
  
  this.addEvents(
    /**
     * @event destroyhive
     * Fires when you want to delete a hive
     * @param {Kohive.HivePanel} this
     */
    'destroy',
    
    /**
     * @event accept
     * Fires when the user had clicked to accept a hive invite
     */
    'accept'
  );
  
  this.render();
};

Ext.extend(Kohive.views.hives.Options, Ext.Panel, {
  hive: null,
  
  onRender : function(ct, pos) {
    var ct  = ct || Ext.getBody();
    
    // Create each of the option buttons
    this.el = ct.createChild({
      cls: 'x-hive-options',
      children: [
        {
          // Information
          tag: 'span',
          cls: 'info',
          children: [
            {
              tag: 'img',
              src: Ext.ux.UrlBuilder.build('resources', '/images/components/HivePanel/info.png'),
              alt: Ext.ux.Translator.translate('information')
            }
          ]
        },
        {
          // Accept
          tag: 'span',
          cls: 'accept',
          children: [
            {
              tag: 'img',
              src: Ext.ux.UrlBuilder.build('resources', '/images/components/HivePanel/accept.png'),
              alt: Ext.ux.Translator.translate('accept')
            }
          ]
        },
        {
          // Delete
          tag: 'span',
          cls: 'delete',
          children: [
            {
              tag: 'img',
              src: Ext.ux.UrlBuilder.build('resources', '/images/components/HivePanel/delete.png')
            }
          ]
        }
      ]
    });
    
    // Define each of the option buttons
    this.infoEl   = this.el.child('span.info');
    this.acceptEl = this.el.child('span.accept');
    this.deleteEl = this.el.child('span.delete');
    
    // Change the display mode for the buttons so they complete disappear instead
    // of just hidding (visibilty:hidden)
    this.infoEl.enableDisplayMode();
    this.acceptEl.enableDisplayMode();
    this.deleteEl.enableDisplayMode();
    
    // Add onClick events to each of the buttons
    this.infoEl.on('click', function() {
      var hs = Kohive.OS.getHiveSummary();
      
      // Check if the hive summary hasn't already been rendered
      if (!hs.rendered) {
        hs.optionsBar = this;
        hs.render();
        hs.showHUD([0, -45]);
        
        return;
      };
      
      hs.optionsBar = this;
      hs.updateContent();
      hs.show();
      hs.positionHUD([0, -45]);
    }, this);
    
    this.acceptEl.on('click', this.fireEvent.createDelegate(this, ['accept',  this]), this);
    this.deleteEl.on('click', this.fireEvent.createDelegate(this, ['destroy', this]), this);
    
    Kohive.views.hives.Options.superclass.onRender.apply(this, arguments);
  },
  
  /**
   * Updates the options bar with the given data and aligns to the button
   * @param {Kohive.HiveButton} button The button to align to
   * @param {Object} data A simple object with the data items referred to in this.tpl (defaults to button.hiveDetails)
   */
  showOptions: function(btn) {
    this.btn  = btn;
    this.hive = btn.hive;
    
    // Updates the visiiblity of each of the buttons
    this.updateOptions();
    
    // Show the OptionBar
    this.show();
  },
  
  /**
   * Removes the options bar visibilty
   */
  hideOptions: function() {
    this.hide();
  },
  
  /**
   * Updates the options element to include each of the option buttons, if needed
   * @see #showOptions
   */
  updateOptions: function() {
    var showInfo   = true;
    var showAccept = false;
    var showDelete = true;
    
    // Check if the hive is pending
    if (this.hive.state == "pending") {
      showInfo = true;
      showAccept = true;
      showDelete = true;
    };
    
    // Do not show the hive information button if it is a personal hive
    if (this.hive.type == "PersonalHive") { showInfo = false; };
    
    // Set the visiblity of each of the options to the correct setting
    this.infoEl.setVisible(showInfo);
    this.acceptEl.setVisible(showAccept);
    this.deleteEl.setVisible(showDelete);
  }
});

/**
 * Kohive.views.hives.Summary
 * @extends Ext.Panel
 * Provides a summary dialog of a given Hive
 */
Kohive.views.hives.Summary = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    hidden:      true,
    id:          'hive-summary',
    cls:         'hive-summary',
    width:       'auto',
    pokerHeight: 44
  });
  
  Kohive.views.hives.Summary.superclass.constructor.call(this, config);
};

Ext.extend(Kohive.views.hives.Summary, Ext.ux.HUD, {
  /**
   * @property template
   * @type Ext.XTemplate
   * The hive summaries template
   */
  template: new Ext.XTemplate(
    '<dl>',
				//       '<tpl if="short_description">',
				//         '<dt>' + Ext.ux.Translator.translate('short_description') + ':</dt>',
				//         '<dd>{short_description}</dd>',
				// '<br />',
				//       '</tpl>',
				//       '<tpl if="!short_description">',
				//         '<dt>' + Ext.ux.Translator.translate('short_description') + ':</dt>',
				//         '<dd>There is no short description</dd>',
				// '<br />',
				//       '</tpl>',
      '<tpl if="owner_nickname">',
        '<dt>' + Ext.ux.Translator.translate('owner') + ':</dt>',
        '<dd>{owner_nickname} / {owner_full_name}</dd>',
      '</tpl>',
			'<tpl if="created_at_date">',
				'<br />',
        '<dt>' + Ext.ux.Translator.translate('created_on') + ':</dt>',
        '<dd>{created_at_date}</dd>',
      '</tpl>',
			'<tpl if="member_count">',
			  '<br />',
        '<dt>' + Ext.ux.Translator.translate('member_count') + ':</dt>',
        '<dd>{member_count}</dd>',
      '</tpl>',
      '<tpl if="invite_hashcode">',
			  '<br />',
        '<dt>' + Ext.ux.Translator.translate('invite_url') + ':</dt>',
        '<dd>http://www.kohive.com/{invite_hashcode}</dd>',
      '</tpl>',
    '</dl>'
  ),
  
  // private
  render: function() {
    if (!this.optionsBar) { return; };
    
    // set the AnchorToEl of the HUD
    this.anchorToEl = this.optionsBar.infoEl;
    
    Kohive.views.hives.Summary.superclass.render.apply(this, arguments);
    
    this.updateContent();
  },
  
  updateContent: function() {
    // Get the hive from the options bar
    var config = this.optionsBar.hive;
    
		// Get the created_at_date
		config.created_at_date = Kohive.OS.convertToDate(config.created_at, true);
		
    // Make sure the message is set to at least null
    config.message = config.message || null;

    // Set the title of the HUD
    this.setTitle(config.title);

    // Compile the template
    var temp = this.template.applyTemplate(config);

    // Append the bwrap
    var bwrap = this.el.child('.' + this.baseCls + '-bwrap');
    bwrap.update(temp);
  },
  
  /**
   * Method for the click listener on the body
   */
  clickListener: function(e) {
    if (!e.getTarget('.x-hud-wrapper')) {
      this.hide();
      Kohive.OS.getHiveOptions().hide();
    };
  }
});

/**
 * Kohive.views.members.Button
 * Customised Button for use in a MemberPanel. Creates a MemberSummary onMouseOver, which is destroyed onMouseOut.
 * @extends Ext.Button
 */
Kohive.views.members.Button = function(member) {
  var config = {};
  
  // Set the memberDetails
  this.member = member;
  
  Kohive.views.members.Button.superclass.constructor.call(this, config);
  
  // Add events and add dragSource to element onRender
  this.on('render', this.initalizeEvents,     this);
  this.on('render', this.initalizeDragSource, this);
};

Ext.extend(Kohive.views.members.Button, Ext.Button, {
  /**
   * Setup the template for a member button
   */
  template: new Ext.Template(
    '<div class="member {type}">',
      '<div class="x-member-avatar">',
        '<div class="x-member-avatar-img"></div>',
        '<div class="x-member-avatar-style"></div>',
      '</div>',
    '</div>'
  ),
  
  // private
  onRender: function(ct, position) {
    var md = this.member.data;
    
    var config = {
      type:   md.state
    };
    
    this.el = this.template.append(ct, config, true);
    
    // Add the avatar to the member
    // TODO remove hack and get real avatar
    // var a = Ext.ux.UrlBuilder.build('resources', '/images/components/MemberPanel/avatars/' + Kohive.OS.getCurrentHive().data.hive.type + '/' + (md.id % 4) + '.png');
    // var a = md.circle_filename;
    
    var defaultAvatar = (Kohive.OS.getCurrentHive().data.hive.type == "PersonalHive") ? md.id_card.hexagon_filename : md.id_card.circle_filename;
    
    var avatarEl = this.el.child('.x-member-avatar-img').applyStyles({
      background: 'url(' + defaultAvatar + ') no-repeat'
    });
		
    // Add the pending clock image if the member is pending
    if (md.state != 'active') {
      this.pendingAvatarEl = this.el.child('.x-member-avatar').createChild({
        cls: 'x-member-avatar-pending'
      });
    };
    
    // Add crown depending on permission
    if (md.access_level > 1) {
      this.createCrown(md.access_level);
    };
    
    // Position the hive panel now that the hive tab has been added
    Kohive.OS.getMemberPanel().positionMemberPanel();
    
    // Adding onClick event to the button
    this.el.on(this.clickEvent, function(e) {
			var target = Ext.get(e.getTarget().id);
			if (!target.hasClass('x-member-avatar-style')) return;
			
			Ext.getCmp('memberOptions').fireEvent('show-idcard', md.id_card_id);
		}, this);
    
    // Register toggle manager
    Ext.ButtonToggleMgr.register(this);
  },
  
  createCrown: function(access_level) {
    if (!this.crownEl) {
      this.crownEl = this.el.child('.x-member-avatar').createChild({
        cls: 'x-member-crown'
      });
    };
    
    this.crownEl.show();
    
    this.crownEl.removeClass('silver');
    this.crownEl.removeClass('gold');
    
    if (access_level == 2) { this.crownEl.addClass('silver'); };
    if (access_level == 3) { this.crownEl.addClass('gold'); };
  },
  
  /**
   * Adds mouseover and mouseout events to the member, which shows the
   * member options panel
   */
  initalizeEvents: function(){
    // Instance of the member options panel
    var mo = Kohive.OS.getMemberOptions();
    
    this.el.unselectable();
    
    // Fire the events on the member options panel
    this.el.on('mouseover', function() { mo.fireEvent('showoptions', this); this.anchorOptions(); }, this);
    this.el.on('mouseout',  function() { mo.fireEvent('hideoptions', this); }, this);
  },
  
  /**
   * Adds drag functionality to the member button
   */
  initalizeDragSource: function(){
    var member = this;
    
    // Check if the member is the user which is currently logged in
    if (this.member.data.id_card.user_id == Kohive.OS.getCurrentUser().data.id) { return; };
    
    // Check if the member is actually a member and not
    // a invite or request
    // if(this.member.data.type == 'active') {
      // Add drag functionality to the member
      this.el.dd = new Ext.dd.DragSource(this.el, {
        
        /**
         * @property ddGroup
         * The drag group the dragsource is part of
         */
        ddGroup: 'memberPanel',
        
        /**
         * @property member
         * Instance of Kohive.models.Membership
         */
        member: this.member,
				btn:    member,
				scroll: false,
        
        /**
         * Called when the user starts dragging a member
         * @param {Ext.Event} e Event
         */
        onDrag: function(e) {
          var dragEl = Ext.get(this.getDragEl());
          var el = Ext.get(this.getEl());
          // var innerEl = el.child('.x-member-avatar');
          
          if (el.dom.innerHTML != dragEl.dom.innerHTML) {
            dragEl.applyStyles({'z-index':20000});
            dragEl.update(el.dom.innerHTML);
          
            dragEl.addClass('x-dragger');
            dragEl.addClass(el.dom.className);
          };
        },
      
        /**
         * Changes the default x+y offset when dragging a member
         * @param {String} x current x position 
         * @param {String} y current y position 
         */
        autoOffset: function(x, y) {
          var memberXY = member.el.getXY();
        
          xDelta = x - memberXY[0];
          yDelta = y - memberXY[1];
        
          this.setDelta(xDelta, yDelta);
        },
      
        /**
         * Called when the member has not been dropped on anything
         */
        afterRepair: function() {
          this.dragging = false;
        }
      });
    // }
  },
  
  /**
   * Anchors the options bar to this buttom
   */
  anchorOptions: function() {
    var mo = Kohive.OS.getMemberOptions();
    
    mo.el.alignTo(this.el, 'b-t');
  }
});
Ext.reg('kohive_member_button', Kohive.views.members.Button);

/**
 * Kohive.views.members.Index
 * @extends Ext.Panel
 * Manages Member Members
 */
Kohive.views.members.Index = function(config) {
  var config = config || {};
	
  Ext.applyIf(config, {
		renderTo: Ext.getBody(),
		border:   false,
		frame:    false,
		hidden:   true,
		
		autoHeight:   false,
	  autoScroll:   false,
		
		trackOver:    true,
	  itemSelector: 'div.member',
		
		listeners: {
			'click': {
				scope: this,
				fn:    this.delegateRowClick
			}
		},
		
	  tpl:          new Ext.XTemplate(
			'<div id="member-panel-wrapper" class="x-member-panel-wrapper">',
			'<div id="member-panel" class="x-member-panel">',
			  '<div>',
					'<tpl for=".">',
						
						//TODO add hexagon_filename integration
						'<div class="member <tpl for="hive">{type}</tpl><tpl for="id_card"><tpl if="is_online || user_id ==  Kohive.OS.getCurrentUser().data.id"> online</tpl><tpl if="!is_online && user_id != Kohive.OS.getCurrentUser().data.id"> offline</tpl></tpl>">',
				      '<div class="x-member-avatar">',
				        '<div class="x-member-avatar-img" style="background:url(<tpl for="id_card">{circle_filename}</tpl>) no-repeat;"></div>',
				        '<div class="x-member-avatar-style<tpl for="id_card"><tpl if="is_online || user_id == Kohive.OS.getCurrentUser().data.id"> online</tpl><tpl if="!is_online && user_id != Kohive.OS.getCurrentUser().data.id"> offline</tpl></tpl>"></div>',
								'<tpl if="!this.isActive(state)"><div class="x-member-avatar-pending"></div></tpl>',
								'<tpl if="access_level == 2"><div class="x-member-crown silver"></div></tpl>',
								'<tpl if="access_level == 3"><div class="x-member-crown gold"></div></tpl>',
				      '</div>',
				    '</div>',
						
			    '</tpl>',
					'<div id="new-member" class="New">',
						'<div class="button"></div>',
					'</div>',
				'</div>',
			'</div>',
			'<div id="new-member-wrapper" class="x-new-member-wrapper"></div>',
			'<div id="member-scroller-left" class="x-scroller-left"></div>',
			'<div id="member-scroller-right" class="x-scroller-right"></div>',
		'</div>', {
			isActive: function(state) {
				return state == "active";
			}
		}
	  )
  });
  
  Kohive.views.members.Index.superclass.constructor.call(this, config);
	
	this.initListeners();
	this.bindWindowResize();
};

Ext.extend(Kohive.views.members.Index, Ext.DataView, {
	/**
	 * The amount (in pixel) the members should be able to scroll
	 * @type Integer
	 */
	scrollAmount: 150,
	
	/**
	 * 
	 */
	initEvents: function() {
		Kohive.views.members.Index.superclass.initEvents.apply(this, arguments);
	},
	
	/**
	 * 
	 */
	initListeners: function() {
		this.on({
			scope        : this,
			'mouseenter' : this.showOptions,
			'mouseleave' : this.hideOptions
		});
	},
	
	/**
	 * 
	 */
	initEverything: function() {
		this.positionMemberPanel();
		// this.setupButtons();
		// if (!this.loadingEl) this.initLoadingEl();
	},
	
	delegateRowClick: function(dataview, index, node, e) {
		var target = Ext.get(e.getTarget()),
		    el     = Ext.get(node);
		    record = dataview.getRecord(node);
		
		if (target.hasClass('x-member-avatar') || target.hasClass('x-member-avatar-img') || target.hasClass('x-member-avatar-style')) return this.onUserClick(record);
	},
	
	/**
	 * Method called when a person clicks on a user, opens the users id card
	 */
	onUserClick: function(record) {
		var controller = Kohive.OS.getController('idcards');
		
		Kohive.models.IdCard.findById(record.get('id_card_id'), {
      scope:   this,
			url:     Kohive.OS.scopedUrl(String.format("id_cards/{0}", record.get('id_card_id'))),
      success: function(idCard) {
        controller.edit(idCard);
      }
    });
	},
	
	/**
	 * Updates the store in the memberpanel (dataview)
	 */
	setStore: function(store) {
		Kohive.views.members.Index.superclass.setStore.apply(this, arguments);
		
		this.initDragSource();
		this.setupButtons();
		
		if (this.rightScroller) this.positionMemberPanel();
	},
	
	/**
	 * 
	 */
	gogogo: function() {
	  Kohive.OS.log('Kohive.views.members.Index: gogogo');
		this.showView();
		this.positionMemberPanel();
	},
	
	/**
	 * Fades in the memberpanel
	 */
	showView: function() {
		this.initEverything();
		
		//hide the loading then send a callback method to be run after
		this.hideLoading();
	},
	
	/**
	 * Hides the memberpanel
	 */
	hideView: function() {
		if (!this.el) return;
		
		this.el.fadeOut({
			scope:    this,
			callback: this.showLoading
		});
	},
	
	/**
	 * Creates a loading icon on the <body>
	 */
	initLoadingEl: function() {
		this.loadingEl = Ext.getBody().createChild({
			cls: 'x-members-loading-icon',
			children: [
				{
				id:  'members-loading-icon'
				}
			]
		});
		
		swfobject.embedSWF("/swf/members-loader.swf", "members-loading-icon", "150", "30", "9.0.0", false, {}, {wmode: "transparent"});
		
		this.loadingEl.enableDisplayMode();
		this.loadingEl.hide();
	},
	
	/**
	 * Shows a loading icon where the members panel is
	 */
	showLoading: function() {
		// if (!this.loadingEl) this.initLoadingEl();
		
		// console.log('show');
		
		// this.loadingEl.fadeIn();
	},
	
	/**
	 * Hides the loading icon and calls the callback method if one is sent
	 */
	hideLoading: function(callback, scope) {
		// console.log('hide');
		// 
		// this.loadingEl.fadeOut({
		// 	scope:    this,
		// 	callback: function() {
				// call the callback if it is set
				if (this.el.isVisible()) return;
				
				this.show();

				this.positionMemberPanel();

				this.el.hide();
				this.el.fadeIn();
		// 	}
		// });
	},
	
	/**
	 * Shows the member options
	 */
	showOptions: function(dataview, index, node, e) {
		var options = Kohive.OS.getMemberOptions(),
				record  = dataview.getRecord(node),
				el      = Ext.get(node);
		
		options.fireEvent('showoptions', this, record, el);
		options.el.alignTo(node, 'b-t');
	},
	
	/**
	 * Hides the member options
	 */
	hideOptions: function(dataview, index, node, e) {
		var options = Kohive.OS.getMemberOptions();
		
		options.fireEvent('hideoptions', e);
	},
	
	/**
	 * 
	 */
	setupButtons: function() {  
    this.newButton     = this.el.child('.New');
    this.leftScroller  = this.el.child('.x-scroller-left');
    this.rightScroller = this.el.child('.x-scroller-right');

		jQuery('#member-scroller-left').hide();
    jQuery('#member-scroller-right').hide();
		
		if (this.newButton) this.newButton.on('click', this.fireEvent.createDelegate(this, ['new', this]), this);
		if (this.leftScroller) this.leftScroller.on('click', function() { this.scrollLeft(); }, this);
    if (this.rightScroller) this.rightScroller.on('click', function() { this.scrollRight(); }, this);
	},
	
	/**
	 * 
	 */
	initDragSource: function() {
		//get all the nodes in the dataview
		var nodes = this.getNodes();
		
		Ext.each(nodes, function(node) {
			var record = this.getRecord(node),
				  el     = Ext.get(node);
			
		  this.addDragSource(record, el, node);
		}, this);
	},
	
	/**
	 * 
	 */
	addDragSource: function(record, el, node) {
    // Check if the member is the user which is currently logged in
    if (record.get('id_card').user_id == Kohive.OS.getCurrentUser().get('id')) return;
    
    //Add drag functionality to the member
    el.dd = new Ext.dd.DragSource(el, {
      
      /**
       * @property ddGroup
       * The drag group the dragsource is part of
       */
      ddGroup: 'memberPanel',
      
      /**
       * @property member
       * Instance of Kohive.models.Membership
       */
      member: record,
			btn:    el,
			scroll: false,
      
      /**
       * Called when the user starts dragging a member
       * @param {Ext.Event} e Event
       */
      onDrag: function(e) {
        var dragEl = Ext.get(this.getDragEl());
        var el = Ext.get(this.getEl());
        // var innerEl = el.child('.x-member-avatar');
        
        if (el.dom.innerHTML != dragEl.dom.innerHTML) {
          dragEl.applyStyles({'z-index':20000});
          dragEl.update(el.dom.innerHTML);
        
          dragEl.addClass('x-dragger');
          dragEl.addClass(el.dom.className);
        };
      },
    
      /**
       * Changes the default x+y offset when dragging a member
       * @param {String} x current x position 
       * @param {String} y current y position 
       */
      autoOffset: function(x, y) {
        var memberXY = el.getXY();
      
        xDelta = x - memberXY[0];
        yDelta = y - memberXY[1];
      
        this.setDelta(xDelta, yDelta);
      },
    
      /**
       * Called when the member has not been dropped on anything
       */
      afterRepair: function() {
        this.dragging = false;
      }
    });
  },
	
	/**
	 * 
	 */
	bindWindowResize: function() {
		var obj = this;
		
		jQuery(window).bind('resize', function() {
      obj.positionMemberPanel();
    });
	},
	
	/**
   * Used to position the member panel depending on its contents.
   * This should probably be in the index but i will move it later
   * @param {Boolean} animate Whether to animate this or not. defaults to false
   * @param {Integer} extra Extra width when adding a tab
   */
  positionMemberPanel: function(animate, extra) {
    // Hack for jQuery as it has no scope
    var obj = this;
    
    // Get the member panel
    var hp = jQuery('#member-panel');
    var extra = extra || 0;
    
    // Get the window and member panel width
    var ww  = jQuery(window).width();
    var hpw = 0;
    var diff = 0;
    
    var members = hp.find('.member');
    members.each(function() {
      var w = jQuery(this).width();
      
      hpw = hpw + w;
    });
    
    hpl = Math.round((ww / 2) - (hpw / 2));
    hpnw = Math.round(hpw + extra + 5);
    
    var first = false;
    
    var nb = jQuery('#new-member');
    
    // check if the width is more than the window
    if ((hpnw + 40) > ww) {
      this.scroller = true;
      
      var nbw = jQuery('#new-member').width();
      
      hpnw = ww - nbw - 1 - 40;
      hpl  = 20;
      
      // move the new member button
      nb.appendTo('#new-member-wrapper');
    } else {
      this.scroller = false;
      var first = true;
      
      nb.appendTo('#member-panel > div');
    };
    
    if (first) {
      var nb = jQuery('#new-member');
      var nbw = jQuery('#new-member').width();
      
      hpnw = hpnw + nbw + 100;
      hpl  = hpl - (nbw / 2);
    };
	  
  	hpnw = hpnw;
  	hpl = hpl;
    
    // Animate the member panel into position
    if (animate) {
      // Decide whether to show the scrollers or not
      if (this.scroller) {
        jQuery('#member-scroller-left').fadeIn();
        jQuery('#member-scroller-right').fadeIn();
      } else {
        jQuery('#member-scroller-left').fadeOut();
        jQuery('#member-scroller-right').fadeOut();
      };
      
      // Animate
      hp.animate({
        left:  hpl,
        width: hpnw
      }, 300, function() {
        obj.scroll(0);
      });
      
      return;
    };
    
    hp.css('left', hpl);
    hp.css('width', hpnw);
    
    // Decide whether to show the scrollers or not
    if (this.scroller) {
      jQuery('#member-scroller-left').show();
      jQuery('#member-scroller-right').show();
    } else {
      jQuery('#member-scroller-left').hide();
      jQuery('#member-scroller-right').hide();
    };
    
    this.scroll(0);
  },
  
  /**
   * Scrolls the scroller left
   */
  scrollLeft: function() {
    if (!this.scroller) return;
    
    this.scroll(this.scrollAmount);
  },
  
  /**
   * Scrolls the scroller right
   */
  scrollRight: function() {
    if (!this.scroller) return;
    
    this.scroll(-this.scrollAmount);
  },
  
  /**
   * Scrolls the scroller, k
   * @param {Integer} scrollAmount The amount you want to scroll
   */
  scroll: function(scrollAmount, end) {
		if (!this.rightScroller) return;
		
    var hp  = jQuery('#member-panel');
    var hps = jQuery('#member-panel > div');
    
		if (!hps || !hps.position) return;
		
    // Get the width of all hive tabs
    var hpsw = 0;
    var hives = hp.find('.member');
    hives.each(function() {
      var w = jQuery(this).width();
      hpsw = hpsw + w;
    });
    
    // get the width of the hive panel
    var hpw = hp.width();
    
    // get the difference between the size of the scroller and the window
    var diff = hpsw - hpw;
    diff = -diff;
    
    // if the end is set, then goto the diff (end)
    if (end) {
      nl = diff;
    } else {
      // get the current left position
      var hpsl = hps.position().left;
      
      // calculate the new left position
      var nl = hpsl + scrollAmount;
      
      // check the boundaries
      if (nl < diff) {
        nl = diff;
        this.rightScroller.addClass('hidden');
        this.leftScroller.removeClass('hidden');
      };
      if (nl >= 0) {
        nl = 0;
        this.rightScroller.removeClass('hidden');
        this.leftScroller.addClass('hidden');
      };
    };
    
    // // Scroll it tbh
    // hps.animate({
    //   left: nl
    // }, 200);
		hps.css('left', nl);
  }
});

Kohive.views.members.New = Ext.extend(Ext.form.HUD, {
	constructor: function(config) {
		var config = config || {};

	  this.saveButton = new Kohive.IconButton({
	    iconCls:  'add',
	    scope:    this,
			disabled: true,
	    handler:  this.fireEvent.createDelegate(this, ['save', this])
	  });

		this.lookupButton = new Kohive.IconButton({
	    iconCls:  'add',
	    scope:    this,
			disabled: true,
	    handler:  function() {
				this.fireEvent(
					'find-contacts',
					Ext.getCmp('new_contacts_email').getValue(),
					Ext.getCmp('new_contacts_password').getValue(),
					this.currentService
				);

				this.hide();
			}
	  });

		this.currentService = "gmail";

	  this.anchorToEl = Kohive.OS.memberPanel.newButton;
		
		//check if max members has been reached
		var max = (Kohive.OS.getCurrentHive().get('hive').max_members == 0) ? 999 : Kohive.OS.getCurrentHive().get('hive').max_members;
		var maxedMembers = Kohive.OS.getCurrentHiveMemberships().length >= max;
		var res = null;
		if (maxedMembers) {
			var isOwner = Kohive.OS.getCurrentUser().get('id') == Kohive.OS.getCurrentHive().get('hive').owner_id;
			
			items = [{
				id:    'new_member_max_members',
	      style: 'margin:10px 15px 15px 15px;text-align:center;',
				html:  (isOwner) ? Ext.ux.Translator.translate('new_pro_hive_max_members_error', {num: max}) : Ext.ux.Translator.translate('new_pro_hive_max_members_error_')
			},
			{
				id:     'new_member_max_members_cols',
	      layout: 'column',
	      style:  'margin:4px 0 15px 55px;',
	      items: [
	        new Kohive.SmallButton({
            text:     Ext.ux.Translator.translate('view_plans'),
            scope:    this,
            style:    'margin-right:5px',
            handler:  this.openSettingsPane
          }),
          new Kohive.SmallButton({
            text:     Ext.ux.Translator.translate('cancel'),
            color:    'red',
            scope:    this,
            handler:  this.destroy
          })
	      ]
	    }];
		} else {
			items = [{
				id:        'new_member_email_text',
				html:      Ext.ux.Translator.translate('first_hive_new_member_desc'),
	      bodyStyle: 'background:transparent;margin:0 10px 5px 10px;'
			},
			{
				id: 'new_member_text'
			},
	    {
				id:     'new_member_email_cols',
	      layout: 'column',
	      style:  'margin:6px 0 12px 0;',
	      items: [
	        {
	          columnWidth: .3,
	          html:        Ext.ux.Translator.translate('email') + ':',
	          bodyStyle:   'background:transparent;margin:0px 5px 0 14px;padding-top:5px;',
	          border:      false
	        },
	        {
	          columnWidth: .6,
	          xtype: 'textfield',
	          id:    'new_member_email',
	          name:  'email',
						width: 175,
	          // vtype: 'email',
	          allowBlank: false
	        },
	        {
	          columnWidth: .2,
	          bodyStyle:   'background:transparent;margin-left:5px;padding-top:2px',
	          border:      false,
	          items: [
	            this.saveButton
	          ]
	        }
	      ]
	    },
			{
				id:     'new_member_contacts_a',
				layout: 'column',
	      style:  'margin:6px 0 10px 10px;',
				items: [
					{
	          xtype: 'kohive_small_button',
	          style: 'text-align:center;margin-right:5px;',
	          cls:   'gmail',
	          color: 'black',
	          text:  'GMail'
	        },
					{
	          xtype: 'kohive_small_button',
	          style: 'text-align:center;margin-right:5px;',
	          cls:   'yahoo',
	          color: 'black',
	          text:  'Yahoo!'
	        },
					{
	          xtype: 'kohive_small_button',
	          style: 'text-align:center;margin-right:5px;',
	          cls:   'hotmail',
	          color: 'black',
	          text:  'Hotmail'
	        },
					{
	          xtype: 'kohive_small_button',
	          style: 'text-align:center;margin-right:10px;',
	          cls:   'plaxo',
	          color: 'black',
	          text:  'Plaxo'
	        }
				]
			},
			{
				id:     'new_member_contacts_b',
				layout: 'column',
	      style:  'margin:0 0 12px 10px;',
				items: [
					{
						html:      Ext.ux.Translator.translate('email') + ':',
	          bodyStyle: 'background:transparent;margin:0px 5px 0 3px;padding-top:3px;',
	          border:    false,
						width:     66
					},
					{
						xtype: 'textfield',
						id:    'new_contacts_email',
						width: 181
					}
				]
			},
			{
				id:     'new_member_contacts_c',
				layout: 'column',
	      style:  'margin:0 0 12px 10px;',
				items: [
					{
						html:      Ext.ux.Translator.translate('password') + ':',
	          bodyStyle: 'background:transparent;margin:0px 5px 0 3px;padding-top:3px;',
	          border:    false,
						width:     66
					},
					{
						xtype:      'textfield',
						id:         'new_contacts_password',
						width:      153,
						inputType:  'password',
						allowBlank: false
					},
					{
	          columnWidth: .2,
	          bodyStyle:   'background:transparent;padding-top:1px;margin:0 0 0 3px;',
	          border:      false,
	          items: [
	            this.lookupButton
	          ]
	        }
				]
			}];
		};

	  Ext.applyIf(config, {
	    title:       Ext.ux.Translator.translate('invite'),
	    cls:         'add-member',
	    id:          'addMemberPanel',
			width:       270,
	    pokerHeight: 14,
	    items: 			 items,

	    //esc key cancels, enter submits
			keys: [
	      {
	        key:   Ext.EventObject.ENTER,
	        fn:    this.onSubmit,
					scope: this
	      },
	      {
	        key:   Ext.EventObject.ESC,
	        fn:    this.fireEvent.createDelegate(this, ['cancel', this]),
					scope: this
	      }
	    ]
	  });

		this.addEvents(
		  /**
		   * @event find-contacts
		   * Fires when the user submits the contacts button
		   */
		  'find-contacts'
		);

		this.tab = 1;
		
		if (maxedMembers) {
			this.on('render', this.removeHeader, this);
		} else {
			this.on('show', this.initTypeEvents,      this);
			this.on('show', this.listenToFieldChange, this);
			this.on('show', this.setupTabs,           this);
			this.on('render', this.addQuestionMark,   this);

		  this.on('show', function() {
				Ext.getCmp('new_member_email').reset();

				Ext.getCmp('new_contacts_email').reset();
				Ext.getCmp('new_contacts_password').reset();

				var tempTask = new Ext.util.DelayedTask(function() {
				  Ext.get('new_member_email').focus();
				}, this);
				tempTask.delay(500);
		  }, this);
		};
		
		Kohive.views.members.New.superclass.constructor.call(this, config);
	},
	
	/**
	 * opens the settings view and opens the accounts view... also destroys the hud
	 */
	openSettingsPane: function() {
	  var controller = Kohive.OS.getController('settings');
	  if (controller) controller.index('settings_view_personal_account');
	  this.destroy();
	},
	
	removeHeader: function() {
		var tempTask = new Ext.util.DelayedTask(function() {
		  var headerEl = this.el.child('.x-hud-header-text');
			headerEl.hide();
		}, this);
		tempTask.delay(10);
	},
	
	onSubmit: function() {
		if (this.tab == 1) {
			this.fireEvent('save', this);
		} else {
			this.fireEvent(
				'find-contacts',
				Ext.getCmp('new_contacts_email').getValue(),
				Ext.getCmp('new_contacts_password').getValue(),
				this.currentService
			);
			
			this.hide();
		};
	},
	
	/**
	 * Adds a question mark (for help) into the header of the HUD
	 */
	addQuestionMark: function() {
		var headerEl = this.el.child('.x-hud-header');
		if (!headerEl) return;
		
		//create the question mark button
		this.questionMarkEl = headerEl.createChild({
			cls:  'x-question-mark',
			html: '?'
		});
		
		this.questionMarkEl.on('click', this.showTooltop, this);
	},
	
	/**
	 * Called when the user presses the question mark on the HUD.
	 */
	showTooltop: function() {
		//get the text element and then get the height before text has entered it
		var textEl = Ext.get('new_member_text');
		var height = {
			before: textEl.getHeight()
		};
		
		//add the new class for styling and update it with the translation
		textEl.addClass('x-hud-text');
		textEl.update(Ext.ux.Translator.translate('new_member_help'));
		
		//get the height after text has been added
		height.after = textEl.getHeight();
		
		//calculate the difference in the heights so we know what to remove from the
		//top of the hud
		var diff = height.before - height.after;
		
		//move the box up Xpx
		this.el.setTop(this.el.getTop() + diff);
		
		this.tooltipShown = true;
	},
	
	tabsSetup: false,
	tab:       null,
	
	setupTabs: function() {
		if (this.tabsSetup) return;
		
		this.tabsSetup = true;
		
		Ext.get('new_member_email_text').enableDisplayMode();
		Ext.get('new_member_email_cols').enableDisplayMode();
		Ext.get('new_member_text').enableDisplayMode();
		Ext.get('new_member_contacts_a').enableDisplayMode();
		Ext.get('new_member_contacts_b').enableDisplayMode();
		Ext.get('new_member_contacts_c').enableDisplayMode();
		
		Ext.get('new_member_contacts_a').hide();
		Ext.get('new_member_contacts_b').hide();
		Ext.get('new_member_contacts_c').hide();
		
		if (!this.firstHive) Ext.get('new_member_email_text').hide();
		
		var btnA = this.el.child('.x-hud-header-text');
		btnA.dom.id = "new_member_goto_a";
		btnA.addClass('x-active');
		
		var headerEl = this.el.child('.x-hud-header');
		var btnB = headerEl.createChild({
			id:   'new_member_goto_b',
			tag:  'span',
			cls:  'x-hud-header-text',
			html: Ext.ux.Translator.translate('iimport')
		});
		
		btnA.on('click', this.switchToEmail, this);
		btnB.on('click', this.switchToContacts, this);
	},
	
	switchToContacts: function(contact) {
		if (this.tab == 2) return;
		
		this.tab = 2;
		
		this.questionMarkEl.hide();
		
		Ext.get('new_member_goto_a').removeClass('x-active');
		Ext.get('new_member_goto_b').addClass('x-active');
		
		Ext.get('new_member_email_cols').hide();
		Ext.get('new_member_text').hide();
		Ext.get('new_member_contacts_a').show();
		Ext.get('new_member_contacts_b').show();
		Ext.get('new_member_contacts_c').show();
		
		Ext.get('new_member_email_text').hide();
		
		//check if the details are there, if so fill out the form
		if (contact && contact.login && contact.password) {
			Ext.getCmp('new_contacts_email').setValue(contact.login);
			Ext.getCmp('new_contacts_password').setValue(contact.password);
			Ext.get('new_contacts_password').focus();
		} else {
			Ext.get('new_contacts_email').focus();
		};
		
		//move the box up Xpx
		var diff = (this.tooltipShown) ? -16 : -54;
		this.el.setTop(this.el.getTop() + diff);
	},
	
	switchToEmail: function() {
		if (this.tab == 1) return;
		
		this.tab = 1;
		
		this.questionMarkEl.show();
		
		Ext.get('new_member_goto_a').addClass('x-active');
		Ext.get('new_member_goto_b').removeClass('x-active');
		
		Ext.get('new_member_email_cols').show();
		Ext.get('new_member_text').show();
		Ext.get('new_member_contacts_a').hide();
		Ext.get('new_member_contacts_b').hide();
		Ext.get('new_member_contacts_c').hide();
		
		if (this.firstHive) Ext.get('new_member_email_text').show();
		
		Ext.get('new_member_email').focus();
		
		//move the box up Xpx
		var diff = (this.tooltipShown) ? 16 : 54;
		this.el.setTop(this.el.getTop() + diff);
	},
	
	/**
   * Adds onClick events to each of the type buttons
   */
  initTypeEvents: function() {
    // Get each of the elements
    this.gmailEl   = this.el.child('.gmail');
    this.yahooEl   = this.el.child('.yahoo');
    this.hotmailEl = this.el.child('.hotmail');
    this.plaxoEl   = this.el.child('.plaxo');
		
    this.gmailEl.on('click', function(e, btn)   { this.changeType('gmail'); this.gmailEl.addClass('pressed');   }, this);
    this.yahooEl.on('click', function(e, btn)   { this.changeType('yahoo'); this.yahooEl.addClass('pressed');   }, this);
    this.hotmailEl.on('click', function(e, btn) { this.changeType('hotmail'); this.hotmailEl.addClass('pressed'); }, this);
    this.plaxoEl.on('click', function(e, btn)   { this.changeType('plaxo'); this.plaxoEl.addClass('pressed');   }, this);
    
		Ext.getCmp('new_member_email').reset();
		
    // Set default onLoad type
    this.changeType('gmail');
    this.gmailEl.addClass('pressed');
  },
	
	/**
   * Changes the type in the hidden input field
   * @param {String} type The type of hive
   */
  changeType: function(type) {
    if (!type) return;
    
    this.gmailEl.removeClass('pressed');
    this.yahooEl.removeClass('pressed');
    this.hotmailEl.removeClass('pressed');
    this.plaxoEl.removeClass('pressed');
    
    this.currentService = type;
  },
	
	/**
	 * Listens to the 2 fields and enables/disables the submit buttons depending if they
	 * are valid or not
	 */
	listenToFieldChange: function() {
		Ext.get('new_member_email').on('keyup', function(item) {
			var item = Ext.getCmp('new_member_email');
			if (!item.isValid()) return this.saveButton.disable();
			
			return this.saveButton.enable();
		}, this);
		
		Ext.get('new_contacts_email').on('keyup', function(item) {
			var item  = Ext.getCmp('new_contacts_email');
			var itemb = Ext.getCmp('new_contacts_password');
			if (!item.isValid() || !itemb.isValid()) return this.lookupButton.disable();
			
			return this.lookupButton.enable();
		}, this);
		
		Ext.get('new_contacts_password').on('keyup', function(item) {
			var item  = Ext.getCmp('new_contacts_email');
			var itemb = Ext.getCmp('new_contacts_password');
			if (!item.isValid() || !itemb.isValid()) return this.lookupButton.disable();
			
			return this.lookupButton.enable();
		}, this);
	}
});

Ext.reg('kohive_member_add_panel', Kohive.views.members.New);

/**
 * Kohive.views.members.Options
 * @extends Ext.Panel
 * Provides a bar on member rollover with options within it
 */
Kohive.views.members.Options = function(config) {
  var config = config || {};
    
  Ext.applyIf(config, {
    id:     'memberOptions',
    cls:    'x-member-options'
  });
  
  this.os = Kohive.OS;
  
  this.addEvents(
    /**
     * @event show-idcard
     * Fired when the user clicks the show Id Card button
     * @param {Number} id The Id Card ID to show
     */
    'show-idcard',
    
    /**
     * @event showsummary
     * Fires when the user clicks the show summary option
     */
    'showsummary',
    
    /**
     * @event hidesummary
     * Fires when the user wants to hide the member summary
     */
    'hidesummary',
    
    /**
     * @event showoptions
     * Fires when the user wants to show the options bar
     */
    'showoptions',
    
    /**
     * @event hideoptions
     * Fires when the user wants to hide the options bar
     */
    'hideoptions',
    
    /**
     * @event acceptmember
     * Fires when the user wants to accept an incoming request
     * @param {Kohive.models.Membership} member Instance of the member
     */
    'acceptmember',
    
    /**
     * @event rejectmember
     * Fires when the user wants to decline an incoming request
     * @param {Kohive.models.Membership} member Instance of the member
     */
    'rejectmember',
    
    /**
     * @event cancelmember
     * Fires when the user wants to cancel an outgoing invite they sent
     * @param {Kohive.models.Membership} member Instance of the member
     */
    'cancelmember',
    
    /**
     * @event kickmember
     * Fires when the user wnats to kick/remove a member from the current hive
     * @param {Kohive.models.Membership} member /instance of the member
     */
    'kickmember'
  );
    
  Kohive.views.members.Options.superclass.constructor.call(this, config);
  
  this.on('showsummary', this.showSummary, this);
  this.on('hidesummary', this.hideSummary, this);
  this.on('showoptions', this.showEvent,   this);
  this.on('hideoptions', this.hideEvent,   this);
  
  this.render();
  
  this.initListeners();
};

Ext.extend(Kohive.views.members.Options, Ext.Container, {
  
  /**
   * Instance of the member button
   */
  btn: null,
  
  /**
   * @property summaryHidden
   * @type Boolean
   * True if the summary is currently hidden
   */
  summaryHidden: true,
  
  // private
  render: function(ct, position){
    var ct = ct || Ext.getBody();
    
    this.el = ct.createChild();
    
    // Button Wrapper
    this.btnWrapper = this.el.createChild({
      tag: 'div',
      cls: 'x-member-options-icons'
    });
    
    // Id Card
    this.idCardEl = this.btnWrapper.createChild({
      tag: 'a',
      cls:  'x-member-options-id-card',
      children: [
        {
          tag: 'img',
          src: Ext.ux.UrlBuilder.build('resources', '/images/components/MemberPanel/id-card.png'),
          alt: Ext.ux.Translator.translate('app_id_card')
        }
      ]
    });
    this.idCardEl.enableDisplayMode();
    
    // Chat - only available if the user is online
    this.chatEl = this.btnWrapper.createChild({
      tag: 'a',
      cls:  'x-member-options-chat',
      children: [
        {
          tag: 'img',
          src: Ext.ux.UrlBuilder.build('resources', '/images/components/MemberPanel/chat.png'),
          alt: 'Chat'
        }
      ]
    });
    this.chatEl.enableDisplayMode();
    
    // Information button
    this.infoEl = this.btnWrapper.createChild({
      tag: 'a',
      cls:  'x-member-options-info',
      children: [
        {
          tag: 'img',
          src: Ext.ux.UrlBuilder.build('resources', '/images/components/Universal/info.png'),
          alt: Ext.ux.Translator.translate('information')
        }
      ]
    });
    this.infoEl.enableDisplayMode();
    
    // Accept
    this.acceptEl = this.btnWrapper.createChild({
      tag: 'a',
      cls:  'x-member-options-accept',
      children: [
        {
          tag: 'img',
          src: Ext.ux.UrlBuilder.build('resources', '/images/components/Universal/accept.png'),
          alt: Ext.ux.Translator.translate('accept')
        }
      ]
    });
    this.acceptEl.enableDisplayMode();
    
    // Delete/Decline
    this.deleteEl = this.btnWrapper.createChild({
      tag: 'a',
      cls:  'x-member-options-delete',
      children: [
        {
          tag: 'img',
          src: Ext.ux.UrlBuilder.build('resources', '/images/components/Universal/delete.png'),
          alt: Ext.ux.Translator.translate('ddelete')
        }
      ]
    });
    this.deleteEl.enableDisplayMode();
    
    // Resend signup email
    this.resendEl = this.btnWrapper.createChild({
      tag: 'a',
      cls:  'x-member-options-resend',
      children: [
        {
          tag: 'img',
          src: Ext.ux.UrlBuilder.build('resources', '/images/components/MemberPanel/reinvite.png'),
          alt: Ext.ux.Translator.translate('resend_signup_email')
        }
      ]
    });
    this.resendEl.enableDisplayMode();
    
    // // Send URL
    // this.sendUrlEl = this.btnWrapper.createChild({
    //   tag: 'a',
    //   cls:  'x-member-options-send-url',
    //   children: [
    //     {
    //       tag: 'img',
    //       src: Ext.ux.UrlBuilder.build('resources', '/images/components/MemberPanel/send-url.png'),
    //       alt: 'Send URL'
    //     }
    //   ]
    // });
    // this.sendUrlEl.enableDisplayMode();
    
    // // Teleport
    // this.teleportEl = this.btnWrapper.createChild({
    //   tag: 'a',
    //   cls:  'x-member-options-teleport',
    //   children: [
    //     {
    //       tag: 'img',
    //       src: Ext.ux.UrlBuilder.build('resources', '/images/components/MemberPanel/teleport.png'),
    //       alt: Ext.ux.Translator.translate('qapp_teleport')
    //     }
    //   ]
    // });
    // this.teleportEl.enableDisplayMode();
    
    // Alias
    this.aliasEl = this.el.createChild({
      tag: 'div',
      cls:  'x-member-options-alias',
      html: ''
    });
    
    // The summary always is visible
    this.infoEl.on('click', function() {
      var ms = this.os.getMemberSummary();
      
      // Check if the member summary hasn't already been rendered
      if (!ms.rendered) {
        ms.optionsBar = this;
        ms.render();
        ms.showHUD([0, -20]);
        
        return;
      };
      
      ms.optionsBar = this;
      ms.updateContent();
      ms.show();
      ms.positionHUD([0, -20], 'b-t');
    }, this);
		
		this.hide();
    
    Kohive.views.members.Options.superclass.render.apply(this, arguments);
  },
  
  /**
   * Sets up listeners on this Panel's internal elements and fires events appropriately
   */
  initListeners: function() {
    this.idCardEl.on('click', function() {
      console.log(this.record.get('id_card_id'));
      this.fireEvent('show-idcard', this.record.get('id_card_id'));
    }, this);
    
    this.acceptEl.on('click', this.fireEvent.createDelegate(this, ['acceptmember', this]));
    this.deleteEl.on('click', this.onDeleteClick, this);
    this.resendEl.on('click', this.onResendClick, this);
    
    //TODO: make sure the id cards controller has been initialized.
    //later versions of MVC should do this automatically so remove when we can...
    Kohive.OS.getController('idcards');
  },
  
  /**
   * This is called when the delete button is clicked and fires events depending on the current state
   * of the panel
   */
  onDeleteClick: function() {
    var os = Kohive.OS;
    
    if (this.record.get('state') == 'active') this.fireEvent('kickmember', this);
    if (this.record.get('state') == 'pending') {
      if (this.record.get('user_id') == os.getCurrentUser().get('id')) {
        this.fireEvent('rejectmember', this);
      } else {
        if (os.hasPermission(true)) this.fireEvent('cancelmember', this);
      }
    }
  },
  
  /**
   * Method called when a user clicks the resend signup email button.
   */
  onResendClick: function() {
    var os = Kohive.OS;
    
    if (this.record.get('state') == 'pending') {
      var user_id = this.record.get('id_card').user_id;
      
      var user = new Kohive.models.User({id:user_id});
      user.save({
        url: user.getResendSignupEmailUrl(),
        success: function() {
          Ext.Msg.alert('', Ext.ux.Translator.translate('resent_signup_email_success'));
        },
        failure: function() {
          Ext.Msg.alert('', Ext.ux.Translator.translate('something_wrong'));
        }
      });
    }
  },
  
  /**
   * Adds listeners to each of the member buttons
   */
  initButtons: function() {
    var os = Kohive.OS;
    
    // Show all buttons
    this.acceptEl.show();
    this.deleteEl.hide();
    this.resendEl.show();
    this.chatEl.hide();
    this.idCardEl.hide();
    
    // Check if it is the current logged in user
    if (this.os.hasPermission() || this.record.get('id_card').user_id == os.getCurrentUser().get('id')) this.deleteEl.show();
    
    // // Check if the user is online
    if (this.record.get('id_card').is_online && this.record.get('id_card').user_id != os.getCurrentUser().get('id')) {
			this.chatEl.show();
			this.chatEl.removeAllListeners();
      this.chatEl.on('click', function() { 
        var controller = Kohive.OS.getController('members');
				controller.fireAction('sendMessage', controller, [this.record, this.el]);
      }, this);
    };
    
    //show and hide buttons based on state
    switch (this.record.get('state')) {
      case 'active':
        this.idCardEl.show();
        this.acceptEl.hide();
        this.resendEl.hide();
        break;
      
      case 'pending':
        if (this.record.get('user_id') != this.os.getCurrentUser().get('id')) {
          this.acceptEl.hide();
        
          if (!this.os.hasPermission([1,2])) this.deleteEl.hide();
        };
      
        this.chatEl.hide();
        break;
    };
		
		// Check if the user is online
    // if (!this.record.get('id_card').is_online || this.aliasEl.dom.innerHTML == Ext.ux.Translator.translate('me')) this.chatEl.hide();
  },
  
  /**
   * Updates the options bar with the given data and aligns to the button
   * @param {Kohive.views.members.Button} member Instance of the member button
   */
  showEvent: function(dataview, record, node) {
    this.record = record;
    this.node   = node;
    
    // If a summary is shown, hide it 
    if (!this.summaryHidden) this.hideSummary();
    
    // Check if the current user is logged in, if it is, change the alias
    if (record.get('id_card').user_id == this.os.getCurrentUser().get('id')) var alias = Ext.ux.Translator.translate('me');
    
    // Update the alias with the new user
    this.aliasEl.dom.innerHTML = (alias || record.get('id_card').nickname) || record.get('id_card').email;
    
    // Get the member options bar and move it into the button element, once
    // that is done, anchor it so it positions correctly
    // this.node.select('.x-member-avatar').insertFirst(this.el);
    
    // Initizalise all the buttons by hiding/showing them all depending on their
    // state and adding listeners to them
    this.initButtons();
    
    // Show the member options
    this.show();
  },
  
  /**
   * Hides the member options bar
   */
  hideEvent: function(e) {
    var el = Ext.get(e.getRelatedTarget());
    if (!el) return;
    
    //remove all listeners from the el
    el.removeAllListeners();
    
    if (el.dom.tagName == "IMG" || el.hasClass('x-member-options-alias') || el.hasClass('x-member-options') || el.parent().hasClass('x-member-options') || el.parent().parent().hasClass('x-member-options') || el.parent().parent().hasClass('x-member-options')) {
      el.on('mouseout', function(e) {
        this.hideEvent(e);
      }, this);
    } else {
      if (this.os.getMemberSummary().hidden || !this.os.getMemberSummary().rendered) this.hide();
    };
    
  }
});

/**
 * Kohive.views.members.Summary
 * @extends Ext.Panel
 * Provides a summary dialog of a given Member
 */
Kohive.views.members.Summary = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    hidden: true,
    id:     'memberSummary',
    cls:    'member-summary',
    header: false
  });
    
  Kohive.views.members.Summary.superclass.constructor.call(this, config);
};

Ext.extend(Kohive.views.members.Summary, Ext.ux.HUD, {
  /**
   * The Ext.Template which is rendered as this panel's contents
   */
  template: new Ext.XTemplate(
    '<dl>',
      '<tpl for="id_card">',
				'<tpl if="user_id_real == user_id">',
	        '<dt>' + Ext.ux.Translator.translate('id_card') + ': </dt>',
	        '<dd>{name}</dd>',
	        '<br />',
	      '</tpl>',
        '<tpl if="!pending">',
          '<dt>' + Ext.ux.Translator.translate('full_name') + ': </dt>',
          '<dd>{full_name}</dd>',
          '<br />',
          '<dt>' + Ext.ux.Translator.translate('nickname') + ': </dt>',
          '<dd>{nickname}</dd>',
          '<br />',
        '</tpl>',
				'<tpl if="email">',
	        '<dt>' + Ext.ux.Translator.translate('email') + ': </dt>',
	        '<dd><a href="mailto:{email}">{email}</a></dd>',
	        '<br />',
				'</tpl>',
        '<tpl if="location && !pending">',
          '<dt>' + Ext.ux.Translator.translate('location') + ': </dt>',
          '<dd>{location}</dd>',
          '<br />',
        '</tpl>',
      '</tpl>',
      '<tpl if="last_seen_at && !pending">',
        '<dt>' + Ext.ux.Translator.translate('last_online') + ': </dt>',
        '<dd>{last_seen_at}</dd>',
        '<br />',
      '</tpl>',
      '<dt>' + Ext.ux.Translator.translate('member_since') + ': </dt>',
      '<dd>{created_at_date}</dd>',
      '<br />',
      '<tpl if="hasPermission && !pending">',
        '<dt class="moderator">' + Ext.ux.Translator.translate('moderator') + ': </dt>',
        '<dd class="moderator"><div<tpl if="access_level &gt; 1"> class="checked" </tpl>></div></dd>',
        '<br />',
      '</tpl>',
    '</dl>'
  ),
  
  // private
  render: function() {
    if (!this.optionsBar) return;
    
    // set the AnchorToEl of the HUD
    this.anchorToEl = this.optionsBar.infoEl;
    
    Kohive.views.members.Summary.superclass.render.apply(this, arguments);
    
    this.updateContent();
  },
  
  /**
   * Updates the content of the member summary with the details of the member from the
   * options bar.
   */
  updateContent: function() {
    // Get the membership from the options bar
    this.member = this.optionsBar.record;
    
    var config = this.member.data;
    
    // Convert the last_seen_at to a date
    config.last_seen_at = false;
    try {
      config.last_seen_at = (config.id_card.user.last_seen_at) ? Kohive.OS.convertToDate(config.id_card.user.last_seen_at, true) : null;
    } catch(e) {};
    
    // Check if the user has permission
    // Check if it is the current logged in user
    config.hasPermission = (Kohive.OS.getCurrentUser().get('id') == config.id_card.user_id) ? false : Kohive.OS.hasPermission(true);
   	
    // Check the state of the user
    config.pending = (config.state == "active") ? false : true;
    config.id_card.pending = (config.state == "active") ? false : true;
		config.id_card.user_id_real = Kohive.OS.getCurrentUser().get('id');
		
    // Compile the template
    var temp = this.template.applyTemplate(config);
    
    // Append the bwrap
    var bwrap = this.el.child('.' + this.baseCls + '-bwrap');
    bwrap.update(temp);
    
    // Get the checkbox for the moderator status, and listen to onClick events on
    // it so we can update the memberships access level
    var checkbox = bwrap.child('.moderator div');
    if (!checkbox) return;
		
    checkbox.on('click', function() {
      var checked = (checkbox.hasClass('checked')) ? true : false;
			
      if (checked === true) checkbox.removeClass('checked');
			else checkbox.addClass('checked');
      
      // Create an instance of the membership using the id, and add the new
      // access_level
      this.member.update({ access_level: (checked) ? '1' : '2' }, {
        url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/moderator',
        success: function() {
          if (checked) {
            this.optionsBar.btn.crownEl.hide();
          } else {
            this.optionsBar.btn.createCrown(2);
          };
        },
        faillure: function(errors) {
          return Ext.Msg.alert(Ext.ux.Translator.translate('change_member_moderator_status'), 'Sorry, something went wrong. Please try again');
        },
        scope: this
      });
    }, this);
  },
  
  /**
   * Method for the click listener on the body
   */
  clickListener: function(e) {
    if (!e.getTarget('.x-hud-wrapper')) {
      this.hide();
      Kohive.OS.getMemberOptions().hide();
    };
  }
});

/**
 * Kohive.views.os.QuickAppsBar
 * Manages positioning of and items within the Quick Apps launcher (right side bar)
 * @extends Ext.Container
 */
Kohive.views.os.QuickAppsBar = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    id: 'quickAppsBar',
    
    defaults: {
      xtype: 'quick_app_shortcut'
    }
  });
  
  Kohive.views.os.QuickAppsBar.superclass.constructor.call(this, config);
  
  this.render();
  
  this.addEvents(
    /**
     * @event showview
     * Fires when the view needs to be acnhored and faded in
     */
    'showview'
  );
  
  //render any new items which have been added or removed
  this.on('add',      this.anchorView, this);
  this.on('remove',   this.anchorView, this);
  this.on('showview', this.showView,   this);
  
  Kohive.OS.on('hiveload', this.populateFromHive, this);
};

Ext.extend(Kohive.views.os.QuickAppsBar, Ext.Container, {
  /**
   * Creates the DOM elements for the Application Launcher
   * @param {Ext.Element} container The container to render this element to (defaults to Ext.getBody())
   */
  render: function(container, position) {
    this.container = container || Ext.getBody();
    
    this.el = this.container.createChild({
      cls: 'x-quick-apps-wrapper',
      children: [
        { cls: 'x-quick-apps'}
      ]
    });

    this.appsHolder = this.el.child('.x-quick-apps');
    
    this.el.hide();
    
    Kohive.views.os.QuickAppsBar.superclass.render.call(this, container, position);
  },
  
  getLayoutTarget: function() {
    return this.appsHolder;
  },
  
  /**
   * Removes any existing apps, adds all quick apps for the given Hive
   * @param {Kohive.models.Hive} The Hive to load quick apps from (defaults to the current hive)
   */
  populateFromHive: function(hive) {
    var hive = hive || Kohive.OS.getCurrentHive();
  },
  
  anchorView: function(){
    this.el.anchorTo(Ext.getBody(), 'r-r', [0, 25]);
    this.doLayout();
  },
  
  /**
   * Anchors then fades the view in after a delay of 1 second
   */
  showView: function() {
    this.anchorView();
    
    this.el.fadeIn({
      duration:1
    });
  }
  
});
Ext.reg('kohive_quick_apps', Kohive.views.os.QuickAppsBar);

/**
 * Kohive.views.os.WidgetBar
 * Manages positioning of and items on the Widget Bar
 * @extends Ext.Panel
 */
Kohive.views.os.WidgetBar = function(config) {
  var config = config || {};
  
  // Widget controls
  this.controlsHive = new Ext.Button({
    toggleGroup:  'widgetControls',
    pressed:      true,
    enableToggle: true,
    template:     new Ext.Template(
      '<div class="x-tabs-hive"></div>'
    ),
    onRender : function(ct, position){
        var btn, targs = [this.text || '&#160;', this.type];

        btn = this.template.append(ct, targs, true);
        var btnEl = btn.child(this.buttonSelector);

        this.initButtonEl(btn, btnEl);
        
        Ext.ButtonToggleMgr.register(this);
    }
  });
  
  this.controlsPerson = new Ext.Button({
    toggleGroup:  'widgetControls',
    enableToggle: true,
    template:     new Ext.Template(
      '<div class="x-tabs-person"></div>'
    ),
    onRender : function(ct, position){
        var btn, targs = [this.text || '&#160;', this.type];

        btn = this.template.append(ct, targs, true);
        var btnEl = btn.child(this.buttonSelector);

        this.initButtonEl(btn, btnEl);
        
        Ext.ButtonToggleMgr.register(this);
    }
  });
  
  this.controlsHive.on('click', this.changePanelToHive, this);
  this.controlsPerson.on('click', this.changePanelToPerson, this);
  
  // Controls
  this.controls = new Ext.Panel({
    region: 'west',
    border: false,
    id:     'widgetControls',
    width:  20,
    items:  [this.controlsHive, this.controlsPerson]
  });
  
  this.hiveWidgets = new Kohive.views.os.WidgetScroller({
    id: 'x-hive-widgets'
  });
  this.personWidgets = new Kohive.views.os.WidgetScroller({
    id: 'x-person-widgets'
  });
  
  // Make the widgets panel
  this.widgets = new Ext.Panel({
    region: 'center',
    border: false,
    id:     'widgets',
    items:  [this.hiveWidgets, this.personWidgets]
  });
  
  // Create the advert
  this.advert = new Ext.Panel({
    id:     'advert',
    region: 'east',
    border: false,
    cls:    'advert',
    html:   '<img src="' + Ext.ux.UrlBuilder.build('resources', '/images/adverts/advert1.jpg') + '" />'
  });
  
  this.changePanelToHive();
  
  Ext.applyIf(config, {
    id:      'widget_bar',
    baseCls: '',
    cls:     'x-widget-bar',
    layout:  'border',
    height:  100,
    border:  false,
    items:   [
      this.controls,
      this.widgets,
      this.advert
    ]
  });
  
  Kohive.views.os.WidgetBar.superclass.constructor.call(this, config);
  
  Kohive.OS.on('hiveload', this.populateFromHive, this);
};

Ext.extend(Kohive.views.os.WidgetBar, Ext.Panel, {
  /**
   * The current panel the widgetBar has displayed
   * @type string
   */
  curentPanel: 'hive',
  
  /**
   * Changes the panel from person > hive or vice versa.
   * @param {String} type
   */
  changePanel: function(type){
    if(type != this.curentPanel){
      // Update the currentPanel
      this.curentPanel = type;
      
      if(type == "hive"){
        this.hiveWidgets.show();
        this.personWidgets.hide();
        
        return;
      }
      
      if(type == "person"){
        this.hiveWidgets.hide();
        this.personWidgets.show();
        
        return;
      }
    }
  },
  
  /**
   * Shortcut for the #changePanel function to change the current widgetbar
   * panel to the hive panel
   * @param {Ext.Button} btn
   * @see #changePanel
   */
  changePanelToHive: function(btn){
    this.changePanel('hive');
    
    // Remove toggle from hive button
    this.controlsHive.enableToggle = false;
    this.controlsPerson.enableToggle = true;
  },
  
  /**
   * Shortcut for the #changePanel function to change the current widgetbar
   * panel to the person panel.
   * @param {Ext.Button} btn
   * @see #changePanel
   */
  changePanelToPerson: function(btn){    
    this.changePanel('person');
    
    // Remove toggle from person button
    this.controlsHive.enableToggle = true;
    this.controlsPerson.enableToggle = false;
  },
  
  /**
   * Populates the widgets associated with the given Hive
   * @param {Kohive.models.Hive} The Hive to load Widgets for
   */
  populateFromHive: function(hive) {
    
  },
  
  /**
   * Anchors then fades the view in after a delay of 1 second
   */
  showView: function() {
    this.el.slideIn();
  },
  
  // private
  render: function() {
    Kohive.views.os.WidgetBar.superclass.render.apply(this, arguments);
    
    this.el.hide();
  }
  
});
Ext.reg('kohive_widget_bar', Kohive.views.os.WidgetBar);

/**
 * Kohive.views.os.WidgetScroller
 * @extends Ext.Panel
 * Scrollable container for any number of Widgets.
 */
Kohive.views.os.WidgetScroller = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    id: 'widget-scroller',
    cls: 'x-widget-scroller'
  });
  
  Kohive.views.os.WidgetScroller.superclass.constructor.call(this, config);
  
  this.on('render', this.onWindowResize, this);
};

Kohive.views.os.WidgetScroller = Ext.extend(Kohive.views.os.WidgetScroller, Ext.BoxComponent, {
  activeWidget:         null,
  enableScroll:         true,
  scrollIncrement:      75,
  scrollRepeatInterval: 50,
  scrollDuration:       .25,
  animScroll:           true,
  resizeWidgets:        false,
  minWidgetWidth:       118,
  widgetMargin:         5,
  widgetWidthSet:       false,
  
  // private
  onRender : function(ct, position){
    Ext.BoxComponent.superclass.onRender.call(this, ct, position);
    
    var ct = ct || Ext.getBody();
    
    // Scroller Element
    this.el = ct.createChild({
      tag: 'div',
      id: this.id,
      cls: this.cls
    });
    
    // Widget Strip Wrapper
    this.stripWrap = this.el.createChild({
      cls: 'x-widget-scroller-buttons-strip-wrap'
    });
    
    // Widget Strip
    this.strip = this.stripWrap.createChild({
      tag: 'ul',
      cls: 'x-widget-scroller-buttons-strip'
    });
    
    // Edge, er?
    this.edge = this.strip.createChild({
      tag:'li',
      cls:'x-widget-scroller-buttons-edge'
    });
    // 
    // this.strip.createChild({
    //   cls:'x-clear'
    // });
  },
  
  /**
   * Checks for window resize and resizes the widget scroller accordingly 
   */
  onWindowResize: function() {
    this.currentWindowWidth = Ext.getBody().getWidth() + 15;
    
    // check for browser resize
    Ext.EventManager.onWindowResize(function(){
      // Get the current width of the 
      var oldWidth = this.currentWindowWidth;
      
      // Update the current width
      this.currentWindowWidth = Ext.getBody().getWidth();
      
      // Calculate the difference
      var difference = this.currentWindowWidth - oldWidth;
      
      // Calculate the new width
      var newWidth = this.stripWrap.getWidth() + difference;
      
      // Set the new width
      this.stripWrap.setWidth(newWidth);
    }, this);
  },
  
  initComponent : function() {
    Kohive.views.os.WidgetScroller.superclass.initComponent.call(this);
    this.on('resize', this.delegateUpdates);
    this.items = [];
  },
  
  add: function(widget) {
    if (!widget) {
      return;
    }
    
    //create the container el
    var li = this.strip.createChild({tag:'li'}, this.edge);
    
    //render the widget to the container
    widget.render(li);
    
    //add the widget to the scrollers items
    this.items.push(widget);
    
    //check the widgets width
    if(!this.widgetWidthSet){
      this.lastWidgetWidth = widget.el.getWidth();
    }
    
    //set the active widget
    this.setActiveWidget(widget);
  },
  
  removeWidget : function(btn){
    var li = document.getElementById(btn.container.id);
    btn.destroy();
    li.parentNode.removeChild(li);

    var s = [];
    for(var i = 0, len = this.items.length; i < len; i++) {
      if(this.items[i] != btn){
        s.push(this.items[i]);
      }
    }
    this.items = s;

    this.delegateUpdates();
  },

  setActiveWidget : function(btn){
    this.activeWidget = btn;
    this.delegateUpdates();
  },

  delegateUpdates : function(){
    /*if(this.suspendUpdates){
    return;
    }*/
    if(this.enableScroll && this.rendered){
      this.autoScroll();
    }
  },

  autoScroll : function(){
    var count = this.items.length;
    var ow = this.el.dom.offsetWidth;
    var tw = this.el.dom.clientWidth;

    var wrap = this.stripWrap;
    var cw = wrap.dom.offsetWidth;
    var pos = this.getScrollPos();
    var l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos;

    if(!this.enableScroll || count < 1 || cw < 20){ // 20 to prevent display:none issues
      return;
    }

    wrap.setWidth(tw); // moved to here because of problem in Safari
    
    if(l <= tw){
      wrap.dom.scrollLeft = 0;
      //wrap.setWidth(tw); moved from here because of problem in Safari
      if(this.scrolling){
        this.scrolling = false;
        this.el.removeClass('x-taskbuttons-scrolling');
        // this.scrollLeft.hide();
        // this.scrollRight.hide();
      }
    }else{
      if(!this.scrolling){
        this.el.addClass('x-taskbuttons-scrolling');
      }
      tw -= wrap.getMargins('lr');
      wrap.setWidth(tw > 20 ? tw : 20);
      
      if(!this.scrolling){
        if(!this.scrollLeft){
          this.createScrollers();
        }else{
          this.scrollLeft.show();
          this.scrollRight.show();
        }
      }
      this.scrolling = true;
      if(pos > (l-tw)){ // ensure it stays within bounds
        wrap.dom.scrollLeft = l-tw;
      }else{ // otherwise, make sure the active button is still visible
        // this.scrollToWidget(this.activeWidget, true); // true to animate
      }
      this.updateScrollWidgets();
    }
  },

  createScrollers : function(){
    var h = this.el.dom.offsetHeight; //var h = this.stripWrap.dom.offsetHeight;

    // left
    var sl = this.el.insertFirst({
      cls:'x-widget-scroller-buttons-scroller-left'
    });
    sl.setHeight(h);
    sl.addClassOnOver('x-widget-scroller-buttons-scroller-left-over');
    this.leftRepeater = new Ext.util.ClickRepeater(sl, {
      interval: this.scrollRepeatInterval,
      handler:  this.onScrollLeft,
      scope:    this
    });
    this.scrollLeft = sl;

    // right
    var sr = this.el.insertFirst({
      cls:'x-widget-scroller-buttons-scroller-right'
    });
    sr.setHeight(h);
    sr.addClassOnOver('x-widget-scroller-buttons-scroller-right-over');
    this.rightRepeater = new Ext.util.ClickRepeater(sr, {
      interval: this.scrollRepeatInterval,
      handler:  this.onScrollRight,
      scope:    this
    });
    this.scrollRight = sr;
  },

  getScrollWidth : function(){
    return this.edge.getOffsetsTo(this.stripWrap)[0] + this.getScrollPos();
  },

  getScrollPos : function(){
    return parseInt(this.stripWrap.dom.scrollLeft, 10) || 0;
  },

  getScrollArea : function(){
    return parseInt(this.stripWrap.dom.clientWidth, 10) || 0;
  },

  getScrollAnim : function(){
    return {
      duration: this.scrollDuration,
      callback: this.updateScrollWidgets,
      scope: this
    };
  },

  getScrollIncrement : function(){
    return (this.scrollIncrement || this.lastWidgetWidth+2);
  },

  scrollToWidget : function(item, animate){
    item = item.el.dom.parentNode; // li
    if(!item){ return; }
    var el = item;
    var pos = this.getScrollPos(), area = this.getScrollArea();
    var left = Ext.fly(el).getOffsetsTo(this.stripWrap)[0] + pos;
    var right = left + el.offsetWidth;
    if(left < pos){
      this.scrollTo(left, animate);
    }else if(right > (pos + area)){
      this.scrollTo(right - area, animate);
    }
  },

  scrollTo : function(pos, animate){
    this.stripWrap.scrollTo('left', pos, true);
  },

  onScrollRight : function(){
    var sw = this.getScrollWidth()-this.getScrollArea();
    var pos = this.getScrollPos();
    var s = Math.min(sw, pos + this.getScrollIncrement());
    if(s != pos){
      this.scrollTo(s, this.animScroll);
    }        
  },

  onScrollLeft : function(){
    var pos = this.getScrollPos();
    var s = Math.max(0, pos - this.getScrollIncrement());
    if(s != pos){
      this.scrollTo(s, this.animScroll);
    }
  },

  updateScrollWidgets : function(){
    var pos = this.getScrollPos();
    this.scrollLeft[pos == 0 ? 'addClass' : 'removeClass']('x-widget-scroller-buttons-scroller-left-disabled');
    this.scrollRight[pos >= (this.getScrollWidth()-this.getScrollArea()) ? 'addClass' : 'removeClass']('x-widget-scroller-buttons-scroller-right-disabled');
  }
});

/**
 * Kohive.views.os.ApplicationLauncher
 * Manages positioning of and items within the Application Launcher (left side bar)
 * @extends Ext.Container
 */
Kohive.views.os.ApplicationLauncher = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
		id: 'application_launcher',
		
    renderTo: Ext.getBody(),
		border:   false,
		frame:    false,
		hidden:   true,
		
		width: 150,
		
		autoHeight:   false,
	  autoScroll:   false,
		
		trackOver:    true,
	  itemSelector: 'div.x-shortcut-application-launcher',
		
		tpl:          new Ext.XTemplate(
			'<div class="x-application-launcher-wrapper">',
				'<div class="x-application-launcher-header"></div>',
			  '<div class="x-application-launcher">',
					'<tpl for=".">',
						'<div class="noselect x-shortcut-application-launcher {[values.title.camelize().toLowerCase()]}">',
				      '<div class="x-shortcut-icon noselect"><img class="noselect" src="/images/components/ApplicationLauncher/icons/{[values.title.camelize().toLowerCase()]}.png" id="app-launcher-shortcut-{[values.title.camelize().toLowerCase()]}-image" /></div>',
							'<tpl if="starred"><a class="x-notification"></a></tpl>',
				    '</div>',
			    '</tpl>',
				'</div>',
				'<div class="x-application-launcher-footer"></div>',
				'<tpl if="this.isAllowed()"><div class="x-application-launcher-add-btn"><button></button></div></tpl>',
		'</div>', {
			isAllowed: function() {
				return Kohive.OS.hasPermission(true);
			}
		}
	  )
  });
	
  Kohive.views.os.ApplicationLauncher.superclass.constructor.call(this, config);
  
	this.initListeners();
};

Ext.extend(Kohive.views.os.ApplicationLauncher, Ext.DataView, {
	loaded: false,
	
	/**
	 * 
	 */
	initListeners: function() {
		Kohive.OS.on({
			scope              : this,
			'hiveload'         : this.populateFromHive,
			'show-first-login' : this.updateShowFirstLogin
		});
		
		this.on({
			scope            : this,
			'click'          : this.onClickEvent,
			'show'           : this.showFirstLogin,
			'containerclick' : this.onContainerClickEvent,
			'mouseenter'     : this.onMouseEnterEvent,
			'mouseleave'     : this.onMouseLeaveEvent
		});
	},
	
	/**
	 * Updates the store in the app launcher (dataview)
	 */
	setStore: function(store) {
		//check if any records in the store are starred
		var store = this.checkIfStarred(store);
		
		Kohive.views.os.ApplicationLauncher.superclass.setStore.apply(this, [store]);
	},
	
	/**
	 * 
	 */
	gogogo: function() {
	  Kohive.OS.log('ApplicationLauncher: gogogo');
		this.showView();
		this.anchorView();
		// this.positionMemberPanel();
	},
	
	/**
	 * Fades in the app launcher
	 */
	showView: function() {
		if (this.isVisible()) return;
		
		this.show();
		
		this.el.hide();
		
		this.el.fadeIn({
		  scope: this,
		  callback: function() {
		    this.fireEvent('show', this);
		  }
		});
		
		//delay it 500 cause the loading animation has to disappear
		// var tempTask = new Ext.util.DelayedTask(function() {
		  // this.el.fadeIn({
				// scope:    this,
				// callback: this.anchorView
			// });
		// }, this);
		// tempTask.delay(500);
	},
	
	/**
	 * Hides the app launcher
	 */
	hideView: function() {
		if (!this.el) return;
		
		this.el.fadeOut();
	},
	
	populateFromHive: function(single) {
	  Kohive.OS.log('populateFromHive');
    var hive = Kohive.OS.getCurrentHive();
    
    // Check if the hive has been found
    if (!hive) return;
    
    // Load the apps for the selected hive
    var applications = hive.applications.findAll();
    
    // When the store loads, loop through each of the applications and add it to the application launcher
    applications.on('load', function(store) {
      Kohive.OS.log('Hive Applications Store: loaded');
			//add feedback app
			var feedback = new Kohive.models.Application({
				id:      68,
				title:   'Feedback',
				starred: false
			});
			store.add(feedback);
			
			//check if it is only a refresh, if so, update the store and continue
			if (single === true) {
				this.setStore(store);
				return this.gogogo();
			};
			
			this.loaded = true;
      Kohive.OS.showMembersAndApplications(null, this, null, store);
      // Kohive.OS.applicationsLoaded = true;
    }, this);
  },
	
	/**
   * Anchors the view to the body
   */
  anchorView: function() {
    this.el.anchorTo(Ext.getBody(), 'l-l', [0, 15]);
    // this.doLayout();
  },
	
	/**
	 * 
	 */
	onClickEvent: function(dataview, index, node, e) {
		var record = dataview.getRecord(node),
				el     = Ext.get(node);
		
		//check if it is feedback
		if (record.get('title') == "Feedback") return feedback_widget.show();
		
		//check if it is starred
		if (record.get('starred')) this.removeStarred(record, el);
		
		//config for dispatcher
		var config = {
			controller: record.get('title').toLowerCase().replace(" ", ""),
			action:     'index'
		};
		
		//fix for chat/chatroom/blog/blogs
		if (config.controller == "chatroom") config.controller = "chat";
		if (config.controller == "blog") config.controller = "blogs";
		
		//refresh the app
		var controller = Kohive.OS.getController(config.controller);
		if (controller) controller.fireAction('refresh');
		
    Kohive.OS.dispatch(config);
	},
	
	/**
	 * 
	 */
	onMouseEnterEvent: function(dataview, index, node, e) {
		if (!this.applicationHud) this.initApplicationHud();
		
		//get the nodes Ext.Element
		var record = dataview.getRecord(node),
		    el     = Ext.get(node);
		
		//update the content of the hud
		var bodyEl = this.applicationHud.el.child('.x-hud-bwrap');
	  bodyEl.update('<div><span>' + Ext.ux.Translator.translate('app_' + record.get('title').underscore()) + '</span></div>');
		
		//set the anchorToEl property of the shortcut + position it
		this.applicationHud.anchorToEl = el;
		this.applicationHud.showHUD([-38, 0], 'l-r', 1, null, false);
	},
	
	/**
	 * 
	 */
	onMouseLeaveEvent: function(dataview, index, node, e) {
		if (this.applicationHud) this.applicationHud.hide();
	},
	
	/**
	 * 
	 */
	initApplicationHud: function() {
		this.applicationHud = new Ext.ux.HUD({
			cls:    'x-application-launcher-hud',
      html:   '',
      header: false,
      width:  'auto',
			poker:  false,
			addClickListenerToBody: Ext.emptyFn
		});
		
		this.applicationHud.render();
		this.applicationHud.hide();
	},
	
	/**
	 * Runs through all the records in the store and checks if any are starred, if there
	 * is any, it checks for the controller and *tries* to refresh the view. if successful
	 * it marked starred as false
	 */
	checkIfStarred: function(store) {
		store.each(function(record) {
			if (record.get('starred')) {
				Kohive.OS.getController('hives').fireAction('refresh');
				
				var controllerName = record.get('title').toLowerCase().replace(" ", ""),
				    controller     = Kohive.OS.getController(controllerName);
				
				if (!controller) return;
				
				//get the view
				var v = controller.indexWindow;
				
				//if there is a view and it is visible, refresh it and mark the record as unstarred
				if (v && v.isVisible()) {
					controller.fireAction('refresh', controller, [this]);
					record.set('starred', false);
					
					this.removeStarred(record);
				};
			};
		}, this);
		
		return store;
	},
	
	/**
	 * 
	 */
	removeStarred: function(record, el) {
		if (record.get('application_hive_memberships_id')) {
	    var model = new Kohive.models.ApplicationHiveMembership({
	      id:      record.get('application_hive_memberships_id'),
	      starred: false
	    });
    	
			//save the application hive model as unstarred
	    model.save();
			
			Kohive.OS.getController('hives').fireAction('refresh');
			
			//remove the notifcation el from the node
			if (el) {
				var star = el.child('a');
				if (star) star.remove();
			};
		};
	},
	
	/**
	 * 	
	 */
	onContainerClickEvent: function(dataview, e) {
		var el = Ext.get(e.getTarget());
		if (!el) return;
		
		if (el.hasClass('x-application-launcher-add-btn') || el.parent().hasClass('x-application-launcher-add-btn')) {			
			Kohive.OS.dispatch({controller:'settings'}, null, ['settings_view_hive_applications']);
		};
	},
	
	/**
	 * 
	 */
	updateShowFirstLogin: function(user) {
	  this.showFirstLoginEnabled = true;
	},
	
	showFirstLoginEnabled: false,
	
	/**
	 * Shows a new HUD which displays information about applications on Kohive. Only shows on first
	 * login to the site.
	 * @param {ExtMVC.Model} user The current user logged into the site
	 */
	showFirstLogin: function() {
	  if (!this.showFirstLoginEnabled) return;
	  
	  this.createHUD(
	    'You can use any of these applications to share things with the people inside this hive. Anything you post in them will be visible by everyone in the hive.<br /><br />Happy sharing!',
	    function() {
	      this.createHUD(
	        "If you have any questions or find any bugs, please don't hesitate to get in touch by clicking here.",
	        null,
	        [-90, -62],
	        'l-br'
	        );
	    });
	  
	  //send a request off to the server to update the user
	  var user = Kohive.OS.getCurrentUser();
	  user.update({
	    shown_first_login: true
	  }, {
	    url: '/update_user'
	  });
	},
	
	createHUD: function(msg, onClickFn, offset, position, scope) {
	  var scope = scope || this;
	  
	  //create a hud
	  var hud = new Ext.ux.HUD({
	    anchorToEl: this.el,
	    poker     : false,
	    title     : false,
	    header    : false,
	    items: [
				{
				  html:      msg,
				  bodyStyle: 'background:transparent;padding:0 5px 5px 5px'
				}
			],
      clickListener: function(e) {
        if (!e.getTarget('.x-hud-wrapper')) {
          this.el.fadeOut({
            scope: this,
            callback: function() {
              if (onClickFn && typeof onClickFn == "function") onClickFn.call(scope);
              onClickFn = null;
              this.destroy();
            }
          });
        };
      }
	  });
	  
		hud.render();
		hud.hide();
		
    // //show the hud
    hud.showHUD(offset || [-90, -20], position || 'l-r', 1, null, true);
	}
});

/**
 * Kohive.DesktopPanel
 * Manages the desktop including icons
 * @extends Ext.Panel
 */
Kohive.DesktopPanel = Ext.extend(Ext.Panel, {
	border:    false,
  bodyStyle: 'background-color: transparent;',
  
  /**
   * Creates the Desktop DOM element
   */
  render: function(container, position) {
    this.container = container || Ext.getBody();
    
    this.el = this.container.createChild({
      tag: 'div',
      cls: 'x-desktop-panel',
      id:  'desktopPanel'
    });
    
    Kohive.DesktopPanel.superclass.render.apply(this, arguments);
  }
});

Ext.reg('desktopPanel', Kohive.DesktopPanel);

/**
 * Kohive.views.os.HeaderPanel
 * @extends Ext.Panel
 * Manages the Settings strip and Snippets bar
 */
Kohive.views.os.HeaderPanel = Ext.extend(Ext.Panel, {
	border:  false,    
  height:  32,
  baseCls: '',
  
  /**
   * Set up the various links and things
   */
  initComponent: function() {
    this.on('render', this.addClickListeners, this);
    Kohive.views.os.HeaderPanel.superclass.initComponent.apply(this, arguments);
    
    // Update the header on hive load
    Kohive.OS.on('hiveload', this.updateInfo, this);
  },
  
  /**
   * Create the various elements used in the header
   */
  render: function(container, position) {
    this.container = container || Ext.getBody();
    
    if (!this.el) {
      this.el = this.container.createChild({
        tag: 'div',
        id:  'headerPanel',
        cls: 'yellow'
      });
    };
    
    this.logoEl = this.el.createChild({
      tag: 'div',
      id:  'logo',
      children: [
        {
          tag: 'img',
          src: Ext.ux.UrlBuilder.build('resources', '/images/components/HeaderPanel/logo.png'),
          alt: Ext.ux.Translator.translate('kohive_logo_alt')
        }
      ]
    });
    
    this.infoEl = this.el.createChild({
      tag: 'div',
      id:  'informationBar',
      children: [
        {
          tag: 'div',
          cls: 'x-header-hive-info',
          children: [
            {
              tag: 'ul', 
              children: [
                {
                  tag: 'li',
                  children: [
                    {
                      tag:  'h1',
                      cls:  'x-header-hive-info-name',
                      html: ''
                    }
                  ]
                },
                {
                  tag: 'li',
                  children: [
                    {
                      tag:  'h2',
                      cls:  'x-header-hive-info-desc',
                      html: ''
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    });
    
    //holder for userDetails, options and language
    this.optionBarEl = this.el.createChild({
      tag: 'div',
      id:  'optionBar'
    });
    
    this.userDetails = this.optionBarEl.createChild({
      tag: 'div',
      cls: 'userDetails',
      children: [
        {
          tag: 'ul',
          children: [
            {
              tag:  'li',
              id:   'userName',
              html: ''
            }
          ]
        }
      ]
    });
    
    var logoutURL = '/logout';
    
    this.optionsEl = this.optionBarEl.createChild({
      tag: 'div',
      cls: 'options',
      children: [
        {
          tag: 'ul',
          children: [
            //{tag: 'li', children: [{tag: 'a', id: 'adjustSettings', title: Ext.ux.Translator.translate('settings_title'), html: Ext.ux.Translator.translate('settings'), href: '#'}]},
            {tag: 'li', cls: 'last', children: [{tag: 'a', id: 'logout', html: Ext.ux.Translator.translate('logout'),   href: logoutURL}]}
            // {tag: 'li', children: [{tag: 'a', id: 'help',           title: Ext.ux.Translator.translate('help_title'),     html: Ext.ux.Translator.translate('help')}]}
          ]
        }
      ]
    });
    
    // this.languageEl = this.optionBarEl.createChild({
    //   tag: 'div',
    //   cls: 'language',
    //   children: [
    //     {
    //       tag: 'ul',
    //       children: [
    //         {tag: 'li', children: [{tag: 'img', 'alt': Ext.ux.Translator.translate('english'), src: Ext.ux.UrlBuilder.build('resources', '/images/header/flags/gb.gif')}]}
    //       ]
    //     }
    //   ]
    // });
    
    Kohive.views.os.HeaderPanel.superclass.render.apply(this, arguments);
  },
  
  /**
   * Adds click listeners to Settings, Logout , Help and the flag
   */
  addClickListeners: function() {
    // Ext.get('adjustSettings').on('click', function(e) {
    //   e.stopEvent();
    //   
    //   // Open the settings application
    //   Kohive.OS.dispatch({controller: 'settings'});
    // });
    
    // Ext.get('help').on('click', function(e) {
    //   e.stopEvent();
    //   alert('help');
    // });
  },
  
  doLogout: function() {
    
  },
  
  doHelp: function() {
    
  },
  
  doSettings: function() {
    
  },
  
  /**
   * Updates the username with current logged in user
   */
  updateUsername: function(){
    var usernameEl = Ext.get('userName');
    usernameEl.update(Ext.ux.Translator.translate('logged_in_as_x', { username: Kohive.OS.getCurrentUser().data.name || Kohive.OS.getCurrentUser().data.email }));
  },
  
  /**
   * Updates the hive information panel (in header) with new data
   * @param {String} name
   * @param {String} desc
   */
  updateInfo: function(hive) {
    if (!hive) { return; };
    
    // Get the hive data
    var hive = hive.data.hive;
    
    // Update the toolbar colour
    this.setBackgroundColour(hive.top_toolbar_colour || null);
    
    // // Get the hives name and description
    // var name = hive.data.name;
    // var shortDescription = hive.data.short_description;
    // 
    // // Get the hive name and description element
    // var hiveInfoEl = this.el.child('.x-header-hive-info');
    // var hiveNameEl = this.el.child('.x-header-hive-info-name');
    // var hiveDescEl = this.el.child('.x-header-hive-info-desc');
    // 
    // return;
    // 
    // // Check if there is a short description
    // if (shortDescription == '') {
    //   hiveInfoEl.addClass('x-no-desc');
    //   
    //   hiveNameEl.update(name);
    //   hiveDescEl.update('');
    // } else {
    //   hiveInfoEl.removeClass('x-no-desc');
    //   
    //   hiveNameEl.update(name);
    //   hiveDescEl.update(shortDescription);
    // };
  },
  
  /**
   * Sets the background of the header panel to the specified colour
   * @param {String} colour The colour to set the background to (one of blue, green, grey, pink or yellow)
   * @param {Object} save True if you want to save the top background colour to the server
   */
  setBackgroundColour: function(colour, save) {
    var colour = (colour || 'yellow').toLowerCase();
    
    Ext.each(['yellow', 'blue', 'green', 'grey', 'pink', 'black', 'purple', 'red', 'lightblue'], function(c) {
      this.removeClass(c);
    }, this);
    
    this.addClass(colour);
    
    // Save it if specified
    if (save) {
      var hive = new Kohive.models.Hive(Kohive.OS.getCurrentHive().data.hive);
      hive.set('top_toolbar_colour', colour);
      
      var config = (typeof save == "object") ? save : {};
      
      Ext.applyIf(config, {
        url: '/hives/' + Kohive.OS.getCurrentHive().data.hive.id
      });
      
      // Save the hive
      hive.save(config);
    };
  }
});

Ext.reg('kohive_header_panel', Kohive.views.os.HeaderPanel);

Kohive.views.blogs.Index = function(config) {
  var config = config || {};
  
	this.controller = Kohive.OS.getController('blogs');
	
  Ext.applyIf(config, {
		title:   Ext.ux.Translator.translate('app_blog'),
		hasTags: true,
		url:     'blog',
    columnPanelConfig: {
      items: [
        {field: 'title',           text: Ext.ux.Translator.translate('title')},
        {field: 'created_at',      text: Ext.ux.Translator.translate('date')},
        {field: 'author_nickname', text: Ext.ux.Translator.translate('creator')}
      ]
    }
  });
  
  // super
  Kohive.views.blogs.Index.superclass.constructor.call(this, Kohive.models.Blog, config);
  
  this.store.on('load', this.onStoreLoad, this);
};
Ext.extend(Kohive.views.blogs.Index, Kohive.views.pod.Index, {
  // Probably not best solution, but for now if an app has a onstoreload method, we init the filterUpdateTask in Kohive.Window
	onStoreLoad: Ext.emptyFn,
	
  // tinyMce: false,
	
  // /**
  //    * Slides the New blog form into view depending on its display status
  //    * @param {Boolean} dontFocus True if you dont want to focus the first field
  //    */
  //   toggleNewForm: function(dontFocus) {
  //     var f = this.newForm || null;
  //      
  //     if (f) {
  //        //check if the form is visible
  //        var visible = f.isVisible();
  //        
  //        if (visible === true) {
  //          f.collapse(false);
  //          f.doLayout();
  //        } else {
  //          f.expand(false);
  //          f.doLayout();
  //          // if (!this.tinyMCE) {
  //            f.hide();
  //          
  //            // tinyMCE.execCommand("mceAddControl", true, "app_blogs_body" + Kohive.OS.getCurrentHiveId());
  //            // this.tinyMCE = true;
  //            
  //            // f.show();
  //            f.collapse(false);
  //          // };
  //        };
  //        
  //        // Get the first item and focus on it
  //        var field = f.find()[0];
  //        if (field) {
  //          var tempTask = new Ext.util.DelayedTask(function() {
  //            field.focus();
  //          }, this);
  //          tempTask.delay(500);
  //        };
  //     };
  //   },
  
  /**
	 * Called when the windows dataview has been clicked. it has to do a few things:
	 *  - toggle the pod
	 *  - delete stuff
	 * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
   * @param {Integer} index The index of the node in question
   * @param {HTMLElement} node The node in question
   * @param {Ext.EventObject} event The event fired
	 */
	viewOnClick: function(dataview, index, node, e) {
	  var el     = Ext.get(node),
	      target = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
	  
	  //Toggle
	  if (target.hasClass('x-control') || target.parent().hasClass('x-control')) {
	    (record.get('toggled') === true) ? record.set('toggled', false) : record.set('toggled', true);
	  };
	  
	  //Delete
	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) {
      var title = el.child('.x-title div');
      if (title) title = title.dom.innerHTML;
      
      var contentEl = el.child('.x-content-wrap');
      
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('app_' + this.model.modelName.toLowerCase().singularize() + '_destroy_desc', { title: title || null }),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") this.controller.fireAction('destroy', this.controller, [record, dataview.store, contentEl]);
        }
      });
	  };
	  
	  //New Comment, initial
    if (target.hasClass('x-button') || target.parent().hasClass('x-button')) {
      //set the records new_comment field to true
      record.set('new_comment', true);
       
      //focus on the new comment field
      var field = Ext.get('blog-' + record.get('id'));
      if (field) this.initField(field, record);
    };
     
    //Toggled comments
    if (target.hasClass('x-toggle-button') || target.parent().hasClass('x-toggle-button')) {
      //set the records new_comment field to true
      record.set('toggled_comments', (record.get('toggled_comments')) ? false : true);
    };
	  
	  //Delete Comment
	  if (target.parent().hasClass('x-delete-comment')) {
	    var el         = Ext.get(target.parent().parent().dom),
	        id         = el.id.split('-')[2] - 1,
	        data       = record.get('comments')[id],
	        comment    = new Kohive.models.Comment(data),
	        controller = Kohive.OS.getController('comments');
	    
	    //set the other_id field to the id being used in the html
	    comment.set('other_id', id + 1);
	    
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('delete_comment_desc'),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") {
            //destroy
            this.showDestroying();
            
            //listen to the controller
            controller.on('destroy',         function(comment) { this.onDestroyCommentSuccess(comment, record); }, this);
            controller.on('destroy-failure', function(comment) { this.onDestroyCommentFailure(); }, this);
            
            //destroy the comment
            controller.destroy(comment);
          };
        }
      });
	  };
	},
	
	/**
	 * 
	 */
	onDestroyCommentSuccess: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.hideLoading();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	  
	  //delete the comment
	  var el = Ext.get('blog-comment-' + comment.get('other_id') + '-' + comment.get('id'));
    if (el) el.fadeOut({remove:true});
    
    //loop through each of the comments on the record and delete the correct one
    var a = [];
    Ext.each(record.get('comments'), function(c) {
      if (c.id != comment.get('id')) a.push(c);
    }, this);
    
    record.set('comments', a);
	},
	
	/**
	 * 
	 */
	onDestroyCommentFailure: function() {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.showFailure();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	},
	
	/**
	 * 
	 */
	initField: function(el, record) {
	  var field  = el.child('.x-field input'),
	      button = el.child('.x-new-comment .x-submit');
	  
	  if (!field || !button) return;
	  
	  //focus the field
	  field.focus();
	  
	  //enable/disable the button on keyup
	  field.on('keyup', function(e) {
	    (field.dom.value.length == 0) ? button.removeClass('x-active') : button.addClass('x-active');
	    
	    //listen for enter key
	    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) {
	      if (button.hasClass('x-active')) this.createComment(record, field.dom.value);
	    };
	    
	    //listen for esc key
	    if (e.getKey() == e.ESC) record.set('new_comment', false);
	  }, this);
	  
	  //enable button click
	  button.on('click', function() { if (button.hasClass('x-active')) this.createComment(record, field.dom.value); }, this);
	},
	
	/**
	 * 
	 */
	createComment: function(record, comment) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //listen for the new comment
	  controller.on('create', this.onCommentCreate, this);
	  
	  //create the comment
	  controller.create(record, comment);
	  
	  //show the loading icon on the window
	  this.showLoading();
	},
	
	/**
	 * 
	 */
	onCommentCreate: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //stop listening to the controller
	  controller.un('create', this.onCommentCreate, this);
	  
	  //get the current membership
	  var membership = Kohive.OS.getCurrentHive();
	  
	  //make the new comment
	  var comment = {
	    created_at:           new Date().format(Kohive.OS.getDefaultDateFormat()),
	    content:              comment.get('content'),
	    author_membership_id: membership.get('id'),
	    author_nickname:      membership.get('id_card').nickname,
	    thumb_filename:       membership.get('id_card').thumb_filename
	  };
	  
	  //add the comment
	  var comments = record.get('comments');
	  comments.unshift(comment);
	  
	  //update a record value to trick extjs into updating the dataview
	  var title = record.get('title');
	  record.set('title', title + '-');
	  record.set('title', title);
	  
	  //remove the new comment field
	  record.set('new_comment', false);
	  
	  //show the success icon on the window
	  this.showSuccess();
	}
});

/**
 * @class Kohive.views.blogs.New
 * @extends Kohive.views.pod.New
 */
Kohive.views.blogs.New = function(config) {
  var config = config || {};
  
  this.tagsField = new Ext.ux.form.SuperBoxSelect({
    fieldLabel:      Ext.ux.Translator.translate('tags'),
    allowAddNewData: true,
    anchor:          '100%',
    name:            'tag_list',
    mode:            'local',
    displayField:    'tag',
    valueField:      'tag',
    listeners: {
      scope: this,
      'enter-key': function(bs) {
        this.onCreate();
      },
      'newitem': function(bs, v) {
        var newObj = {
            id:   v,
            name: v,
            tag:  v
        };
        bs.addItem(newObj);
      }
    }
	});
  
  Ext.applyIf(config, {
    defaults: {
      anchor: "-15"
    },
		items: [
			{
		    xtype:      'textfield',
				id:         'app_blogs_title' + Kohive.OS.getCurrentHiveId(),
		    fieldLabel: Ext.ux.Translator.translate('title'),
		    name:       'title',
		    allowBlank: false
		  },
      {
        xtype:      'textarea',
        id:         'app_blogs_body' + Kohive.OS.getCurrentHiveId(),
        fieldLabel: Ext.ux.Translator.translate('post'),
        name:       'body',
        height:     100,
				grow:       true,
				growMin:    100,
		    enableKeyEvents: true,
		    listeners: {
		      'keydown': {
		        fn: function(field, e) {
		          var key = Ext.EventObject.getKey();
		          if (key === 13){
		            e.stopPropagation();
								if (Ext.EventObject.ctrlKey) this.onCreate();
		          }
		        },
		        scope: this
		      }
		    }
      },
		  this.tagsField
		],
		
    // panel: false,
		submitButtonConfig: {
			text: Ext.ux.Translator.translate('post')
		}
  });
  
  Kohive.views.blogs.New.superclass.constructor.call(this, Kohive.models.Blog, config);
	
  this.on('render', this.initResetStuff, this);
};
Ext.extend(Kohive.views.blogs.New, Kohive.views.pod.New, {
  initResetStuff: function() {
    var form = this.getForm();

    form.on('reset', function() {
      // var inst = tinyMCE.getInstanceById("app_blogs_body" + Kohive.OS.getCurrentHiveId());
      // inst.setContent('');

      //focus on the title field
      Ext.getCmp('app_blogs_title' + Kohive.OS.getCurrentHiveId()).focus();
    }, this);
  },
	
	/**
   * Called when the save button is clicked or CTRL + s pressed.  By default this simply fires
   * the associated controller's 'update' action, passing this.getForm() as the sole argument
   */
  onCreate: function() {
    var form = this.getForm();
    
		//get the tinymce body field valid
    // var inst = tinyMCE.getInstanceById("app_blogs_body" + Kohive.OS.getCurrentHiveId());
    // var val = (inst) ? inst.getContent() : '';
	
    // form.setValues({
    //  'body_html': val
    // });
		
		//tags
    form.items.each(function(item) {
      if (item.name == "tag_list") {
        var val = item.el.dom.value;
        if (!val) return;
        
        item.view.clearSelections();
        item.collapse();
        item.setRawValue('');
        item.fireEvent('newitem', item, val);
      };
    }, this);
		
    // Check if the form is valid
    if (form.isValid()) {
      this.controller.fireAction('create', null, [form]);
      this.doLayout();
    };
  }
	
  // toggleNewForm: function() {
  //  //check if the form is visible
  //  var visible = this.isVisible();
  //  
  //  if (visible === true) {
  //    this.hide();
  //  } else {
  //    if (!this.tinyMCE) {
  //      this.expand(false);
  //      this.hide();
  //    
  //      tinyMCE.execCommand("mceAddControl", true, "app_blogs_body" + Kohive.OS.getCurrentHiveId());
  //      this.tinyMCE = true;
  //      
  //      // f.show();
  //      // f.collapse(false);
  //    };
  //    
  //    this.show();
  //  };
  //  
  //  // Get the first item and focus on it
  //  var field = this.find()[0];
  //  if (field) {
  //    var tempTask = new Ext.util.DelayedTask(function() {
  //      field.focus();
  //    }, this);
  //    tempTask.delay(500);
  //  };
  // }
});

Kohive.views.bookmarks.Index = function(config) {
  var config = config || {};
  
	this.controller = Kohive.OS.getController('bookmarks');
	
  Ext.applyIf(config, {
    hasTags: true,
    columnPanelConfig: {
      items: [
        {field: 'title',           text: Ext.ux.Translator.translate('title')},
        {field: 'url',             text: Ext.ux.Translator.translate('url')},
        {field: 'created_at',      text: Ext.ux.Translator.translate('date')},
        {field: 'author_nickname', text: Ext.ux.Translator.translate('creator')}
      ]
    }
  });
  
  // super
  Kohive.views.bookmarks.Index.superclass.constructor.call(this, Kohive.models.Bookmark, config);
  
  this.store.on('load', this.onStoreLoad, this);
};
Ext.extend(Kohive.views.bookmarks.Index, Kohive.views.pod.Index, {
  // Probably not best solution, but for now if an app has a onstoreload method, we init the filterUpdateTask in Kohive.Window
	onStoreLoad: Ext.emptyFn,
	
	/**
   * Overrides and adds to the default header toolbar buttons
   * @return {Array} The header buttons
   */
  getDefaultHBar: function() {
    var buttons = Kohive.views.microblogs.Index.superclass.getDefaultHBar.apply(this, arguments);
    
    /**
     * @property deliciousButton
     * @type Ext.Button
     * The Delicious Sync toggle button
     */
    this.deliciousButton = new Ext.ux.HeaderToolbarButton({
      text:    Ext.ux.Translator.translate('delicious_sync'),
      iconCls: 'delicious',
			disabled: true
    });
		
		/**
     * @property diggButton
     * @type Ext.Button
     * The Digg Sync toggle button
     */
    this.diggButton = new Ext.ux.HeaderToolbarButton({
      text:    Ext.ux.Translator.translate('digg_sync'),
      iconCls: 'digg',
			disabled: true
    });
    
		return [
      {
        text:    Ext.ux.Translator.translate('nnew'),
        iconCls: 'new',
        xtype:   'header_toolbar_button',
        handler: this.toggleNewForm,
        scope:   this
      },
      {
        text:    Ext.ux.Translator.translate('refresh'),
        iconCls: 'refresh',
        xtype:   'header_toolbar_button',
        handler: this.controller.fireAction.createDelegate(this.controller, ['refresh', this.controller])
      },
      {
        text:    Ext.ux.Translator.translate('toggle_all'),
        iconCls: 'toggle-all',
        xtype:   'header_toolbar_button',
        handler: this.toggleAll,
        scope:   this
      },
      // this.deliciousButton,
      // this.diggButton,
      '->',
			{
        xtype: 'searchfield',
				listeners: {
					scope: this,
					'keyup' : this.searchStore
				}
      }
    ];
  },
  
  /**
	 * Called when the windows dataview has been clicked. it has to do a few things:
	 *  - toggle the pod
	 *  - delete stuff
	 * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
   * @param {Integer} index The index of the node in question
   * @param {HTMLElement} node The node in question
   * @param {Ext.EventObject} event The event fired
	 */
	viewOnClick: function(dataview, index, node, e) {
	  var el     = Ext.get(node),
	      target = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
	  
	  //Toggle
	  if (target.hasClass('x-control') || target.parent().hasClass('x-control')) {
	    (record.get('toggled') === true) ? record.set('toggled', false) : record.set('toggled', true);
	  };
	  
	  //Delete
	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) {
      var title = el.child('.x-title div');
      if (title) title = title.dom.innerHTML;
      
      var contentEl = el.child('.x-content-wrap');
      
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('app_' + this.model.modelName.toLowerCase().singularize() + '_destroy_desc', { title: title || null }),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") this.controller.fireAction('destroy', this.controller, [record, dataview.store, contentEl]);
        }
      });
	  };
	  
	  //New Comment, initial
    if (target.hasClass('x-button') || target.parent().hasClass('x-button')) {
      //set the records new_comment field to true
      record.set('new_comment', true);
       
      //focus on the new comment field
      var field = Ext.get('bookmark-' + record.get('id'));
      if (field) this.initField(field, record);
    };
     
    //Toggled comments
    if (target.hasClass('x-toggle-button') || target.parent().hasClass('x-toggle-button')) {
      //set the records new_comment field to true
      record.set('toggled_comments', (record.get('toggled_comments')) ? false : true);
    };
	  
	  //Delete Comment
	  if (target.parent().hasClass('x-delete-comment')) {
	    var el         = Ext.get(target.parent().parent().dom),
	        id         = el.id.split('-')[2] - 1,
	        data       = record.get('comments')[id],
	        comment    = new Kohive.models.Comment(data),
	        controller = Kohive.OS.getController('comments');
	    
	    //set the other_id field to the id being used in the html
	    comment.set('other_id', id + 1);
	    
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('delete_comment_desc'),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") {
            //destroy
            this.showDestroying();
            
            //listen to the controller
            controller.on('destroy',         function(comment) { this.onDestroyCommentSuccess(comment, record); }, this);
            controller.on('destroy-failure', function(comment) { this.onDestroyCommentFailure(); }, this);
            
            //destroy the comment
            controller.destroy(comment);
          };
        }
      });
	  };
	},
	
	/**
	 * 
	 */
	onDestroyCommentSuccess: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.hideLoading();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	  
	  //delete the comment
	  var el = Ext.get('bookmark-comment-' + comment.get('other_id') + '-' + comment.get('id'));
    if (el) el.fadeOut({remove:true});
    
    //loop through each of the comments on the record and delete the correct one
    var a = [];
    Ext.each(record.get('comments'), function(c) {
      if (c.id != comment.get('id')) a.push(c);
    }, this);
    
    record.set('comments', a);
	},
	
	/**
	 * 
	 */
	onDestroyCommentFailure: function() {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.showFailure();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	},
	
	/**
	 * 
	 */
	initField: function(el, record) {
	  var field  = el.child('.x-field input'),
	      button = el.child('.x-new-comment .x-submit');
	  
	  if (!field || !button) return;
	  
	  //focus the field
	  field.focus();
	  
	  //enable/disable the button on keyup
	  field.on('keyup', function(e) {
	    (field.dom.value.length == 0) ? button.removeClass('x-active') : button.addClass('x-active');
	    
	    //listen for enter key
	    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) {
	      if (button.hasClass('x-active')) this.createComment(record, field.dom.value);
	    };
	    
	    //listen for esc key
	    if (e.getKey() == e.ESC) record.set('new_comment', false);
	  }, this);
	  
	  //enable button click
	  button.on('click', function() { if (button.hasClass('x-active')) this.createComment(record, field.dom.value); }, this);
	},
	
	/**
	 * 
	 */
	createComment: function(record, comment) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //listen for the new comment
	  controller.on('create', this.onCommentCreate, this);
	  
	  //create the comment
	  controller.create(record, comment);
	  
	  //show the loading icon on the window
	  this.showLoading();
	},
	
	/**
	 * 
	 */
	onCommentCreate: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //stop listening to the controller
	  controller.un('create', this.onCommentCreate, this);
	  
	  //get the current membership
	  var membership = Kohive.OS.getCurrentHive();
	  
	  //make the new comment
	  var comment = {
	    created_at:           new Date().format(Kohive.OS.getDefaultDateFormat()),
	    content:              comment.get('content'),
	    author_membership_id: membership.get('id'),
	    author_nickname:      membership.get('id_card').nickname,
	    thumb_filename:       membership.get('id_card').thumb_filename
	  };
	  
	  //add the comment
	  var comments = record.get('comments');
	  comments.unshift(comment);
	  
	  //update a record value to trick extjs into updating the dataview
	  var title = record.get('title');
	  record.set('title', title + '-');
	  record.set('title', title);
	  
	  //remove the new comment field
	  record.set('new_comment', false);
	  
	  //show the success icon on the window
	  this.showSuccess();
	}
});

Kohive.views.bookmarks.New = function(config) {
  var config = config || {};
  
  this.tagsField = new Ext.ux.form.SuperBoxSelect({
    fieldLabel:      Ext.ux.Translator.translate('tags'),
    allowAddNewData: true,
    name:            'tag_list',
    width:           '98%',
    mode:            'local',
    displayField:    'tag',
    valueField:      'tag',
    listeners: {
      scope: this,
      'enter-key': function(bs) {
        this.onCreate();
      },
      'newitem': function(bs, v) {
        var newObj = {
            id:   v,
            name: v,
            tag:  v
        };
        bs.addItem(newObj);
      }
    }
	});
  
  // super
  Kohive.views.bookmarks.New.superclass.constructor.call(this, Kohive.models.Bookmark, {
    panel: false,
    defaults: {
      anchor: "-15"
    },
		items: [
		  {
		    xtype:      'textfield',
		    fieldLabel: Ext.ux.Translator.translate('url'),
		    name:       'url',
		    vtype:      'urlWithoutHttp',
		    allowBlank: false,
		    width:      '98%',
		    listeners: {
		      scope:   this,
		      'change': this.onUrlUpdate
		    }
		  },
			{
		    xtype:      'textfield',
		    fieldLabel: Ext.ux.Translator.translate('title'),
		    name:       'title',
		    allowBlank: false,
		    width:      '98%'
		  },
		  {
		    xtype:      'textarea',
		    fieldLabel: Ext.ux.Translator.translate('notes'),
		    name:       'note',
		    width:      '98%',
		    height:     50,
				grow:       true,
				growMin:    50,
		    enableKeyEvents: true,
		    listeners: {
		      'keydown': {
		        fn: function(field, e) {
		          var key = Ext.EventObject.getKey();
		          if (key === 13){
		            e.stopPropagation();
								if (Ext.EventObject.ctrlKey) this.onCreate();
		          }
		        },
		        scope: this
		      }
		    }
		  },
		  this.tagsField
		],
		submitButtonConfig: {
			text: Ext.ux.Translator.translate('add')
		}
  });
};
Ext.extend(Kohive.views.bookmarks.New, Kohive.views.pod.New, {
  updateTimer:  null,
  currentValue: null,
  gotTitle:     false,
  
  onUrlUpdate: function(field) {
    var scope      = this,
        value      = field.getValue(),
        titleField = scope.items.items[1];
    
    if (value == this.currentValue) return;
    this.currentValue = value;
    
    clearTimeout(this.updateTimer);
    this.updateTimer = setTimeout(function() {
      if (!field.isValid()) return;
      
      //set the feild as 'loading'
      titleField.emptyText = Ext.ux.Translator.translate('app_bookmarks_loading_title');
      titleField.setValue('');
      
      a = new Kohive.models.UrlInfo({address:value});
      a.save({
        url: 'tools/get_url_info',
        scope: this,
        success: function(record, response) {
          var response = Ext.decode(response.responseText),
              title    = response.title;
          
          if (titleField && title) titleField.setValue(title);
          titleField.fireEvent('change', titleField);
          titleField.emptyText = "";
        },
        failure: function() {
          titleField.setValue('');
          titleField.fireEvent('change', titleField);
        }
      });
    }, 400);
  }
});

/**
 * Overrides the default display name to show the Kohive nickname
 * @return {String} The name to display for this member
 */
Ext.ux.IRC.Member.prototype.displayName = function() {
  return this.nickname.split('--')[0];
};

/**
 * @class Kohive.views.chat.Index
 * @extends Kohive.Window
 * The main chat rooms window
 */
Kohive.views.chat.Index = Ext.extend(Kohive.Window, {
  
  constructor: function(config) {
    /**
     * @property ircManager
     * @type Ext.ux.IRC.Manager
     * The IRC Manager instance
     */
    this.ircManager = Kohive.OS.ircManager;
    
    /**
     * @property usersPanel
     * @type Kohive.views.chat.UsersPanel
     * The IRC plugin's Channel Panel
     */
    this.usersPanel = new Kohive.views.chat.UsersPanel({
      baseCls: 'x-window-side-panel',
      border:  false,
      header:  false,
      defaults: {
        border: false
      },
      manager: this.ircManager
    });
    
    /**
     * @property currentChannel
     * @type Ext.ux.IRC.Channel
     * The channel the user is currently 'in' (e.g. the channel to send messages to)
     */
    this.currentChannel = null;
    
    /**
     * @property chatPanel
     * @type Ext.ux.IRC.ChatPanel
     * The IRC plugin's Chat Panel, which contains a chat history panel
     */
    this.chatPanel = new Kohive.views.chat.ChatPanel({
      region: 'center',
      title:  null,
      listeners: {
        'send-message': {
          scope: this,
          fn: function(messageText) {
            this.ircManager.proxy.sendPrivMsg(this.currentChannel.name, messageText);
          }
        }
      }
    });
    
    /**
     * @property historyPanel
     * @type Ext.ux.IRC.HistoryPanel
     * Reference to the ChatPanel's HistoryPanel
     */
    this.historyPanel = this.chatPanel.historyPanel;
    
    Ext.applyIf(config, {
      id:     'chat_index' + Kohive.OS.getCurrentHive().data.hive_id,
      url:    'chatroom',
      layout: 'border',
      items:  [
        this.usersPanel,
        this.chatPanel
      ],
      
      title:  Ext.ux.Translator.translate('app_chat_room'),
      hbar:   false,
      bbar:   false,
      width:  380,
      height: 400,
      
      filterPanelConfig: false,
      plugins: [new Ext.ux.WindowSidePanel({
        windowSidePanelHidden: false,
        windowSidePanel: this.usersPanel
      })],
      
      closeAction: 'closeByUser'
    });
    
    Kohive.views.chat.Index.superclass.constructor.call(this, config);
    
    this.initListeners();
    
    this.channelName = Kohive.OS.getController('chat').currentHiveChannelName();
    
    try {
      var chan = this.ircManager.findChannel(this.channelName);
      this.onChannelChanged(chan);      
    } catch(e) {};
  },
  
  /**
   * Called whenever the user changes channel - tells History panel and channel panel to update themselves
   * @param {Ext.ux.IRC.Channel} channel The channel switched to
   */
  onChannelChanged: function(channel) {
    if (channel.name == this.channelName) {
      this.currentChannel = channel;
      this.historyPanel.setCurrentChannel(channel);
      this.usersPanel.setActiveChannel(channel);      
    }
  },
    
  /**
   * Listen to IRC events and set up responses
   */
  initListeners: function() {
    Kohive.OS.ircManager.on({
      scope           : this,
      'connected'     : this.joinChannel, // in case chat app is opened before the irc client could connect itself
      'channel-joined': this.onChannelChanged
    });
    
    Kohive.views.chat.Index.superclass.initListeners.apply(this, arguments);
    
    this.on('show', this.joinChannel);
    // can't call this.ownerCt from within usersPanel, so I add the event listerner here
    // this.on('show', this.usersPanel.buildElements, this.usersPanel);
    this.on('closebyuser', this.leaveChannel);
    this.on('show', function() {
			this.historyPanel.scrollToBottom();
		});
  },
  
  /**
   * Join channel if connect, since when reloading the page with chat app in the url,
   * this method could be called before the irc proxy is connected
   */
  joinChannel: function() {
    if (Kohive.OS.ircManager.proxy.connected)
      Kohive.OS.ircManager.join(Kohive.OS.getController('chat').currentHiveChannelName());
  },
  
  leaveChannel: function() {
    Kohive.OS.ircManager.leave(Kohive.OS.getController('chat').currentHiveChannelName());
  },
  
  ircUserNameToKohiveUserName: function(ircUserName) {
    var kohiveUserName, userId = ircUserName.split('--')[1];
    Ext.each(Kohive.OS.getCurrentHiveMemberships(), function (membership) {
      if (membership.data.id_card.user_id == parseInt(userId)) {
        kohiveUserName = membership.data.id_card.nickname;
        return false;
      }
    });
    return kohiveUserName;
  },
  
  getDataViewConfig: Ext.emptyFn
});

/**
 * @class Kohive.views.chat.ChatPanel
 * @extends Ext.Panel
 * Displays the current chat and a send message form
 */
Kohive.views.chat.ChatPanel = Ext.extend(Ext.Panel, {
  
  constructor: function(config) {
    config = config || {};
    
    Ext.applyIf(config, {
      historyPanelConfig    : {},
      sendMessagePanelConfig: {}
    });
    
    Ext.apply(this, {
      sendMessageInputId : 'irc-message-to-send-' + new Date().format('is'),

      sendMessageSubmitId: 'irc-message-send-button-' + new Date().format('is'),
      
      /**
       * @property sentMessages
       * @type Array
       * Array of messages sent so far this session, with newest messages first
       */
      sentMessages: [],

      /**
       * @property sentMessageHistoryIndex
       * @type Number
       * The index of the sentMessages array currently being shown in the textarea
       */
      sentMessageHistoryIndex: -1
    });
    
    this.historyPanelConfig = Ext.applyIf(config.historyPanelConfig, {
      region:     'center',
      autoScroll: true,
			border:     false
    });
    
    this.sendMessagePanelConfig = Ext.applyIf(config.sendMessagePanelConfig, {
      region:     'south',
      split:      true,
			autoHeight: true,
      border:     false,
      html: {
        cls: 'irc-chat-message',
        children: [
          {
            cls:      'irc-chat-message-input-wrapper',
            children: [
		          {
		            tag:      'input',
		            id:       this.sendMessageInputId,
		            tabindex: 1,
								disabled: true
		          }
		        ]
          }
        ]
      }
    });
    
    Kohive.views.chat.ChatPanel.superclass.constructor.apply(this, arguments);
  },

  initComponent: function() {
    /**
     * @property historyPanel
     * @type Kohive.views.chat.HistoryPanel
     * The panel used to display chat history. Supply config options in historyPanelConfig
     */
    this.historyPanel = new Kohive.views.chat.HistoryPanel(this.historyPanelConfig);
    
    /**
     * @property sendMessagePanel
     * @type Ext.Panel
     * The panel used to allow the user to send a message. Supply config options in sendMessagePanelConfig
     */
    this.sendMessagePanel = new Ext.Panel(this.sendMessagePanelConfig);

    Ext.applyIf(this, {
      title:  'Connecting...',
      cls:    'irc-chat',
      layout: 'border',
      items:  [this.historyPanel, this.sendMessagePanel]
    });
    
    Kohive.views.chat.ChatPanel.superclass.initComponent.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event send-message
       * Fired when the enter key is pressed inside the textarea
       * @param {String} text The text currently in the textarea
       */
      'send-message'
    );

		this.initListeners();
  },
	
	/**
   * Called whenever the user changes channel - tells History panel and channel panel to update themselves
   * @param {Ext.ux.IRC.Channel} channel The channel switched to
   */
  enableMessageInput: function(channel) {
    if (Kohive.OS.ircManager.proxy.connected) {
      var field = Ext.get(this.sendMessageInputId);
      if (field) {
				field.dom.disabled = false;
				field.focus();
			};
    }
  },
	
  /**
   * Listen to IRC events and set up responses
   */
  initListeners: function() {
    Kohive.OS.ircManager.on({
      scope           : this,
      'channel-joined': this.enableMessageInput
    });
		
		this.on('render', this.setupTextarea, this);
  },
  
  /**
   * Sets up the textarea input behaviour.  Run this after render
   */
  setupTextarea: function() {
    var tempTask = new Ext.util.DelayedTask(function() {
      this.msgInput = Ext.get(this.sendMessageInputId);
			if (!this.msgInput) return;

	    this.msgInput.dom.onkeydown = this.handleKeyPress.createDelegate(this);
    }, this);
    tempTask.delay(500);
  },
  
  /**
   * Listens to clicks and keypresses on the send button
   */
  setupSendButton: Ext.emptyFn,
  
  /**
   * Listens for special key presses, fires the appropriate actions
   * @param {Ext.EventObject} event The key event
   */
  handleKeyPress: function(event) {
    var code = event.keyCode, eo = Ext.EventObject;
    
    switch (code) {
      case eo.ENTER : return this.sendMessage();    break;
      case eo.UP    : return this.historyBack();    break;
      case eo.DOWN  : return this.historyForward(); break;
    }
  },
  
  /**
   * Sends the message currently inside the textarea, adds to history
   */
  sendMessage: function() {
    var message = this.msgInput.getValue().replace(/\n/, '');
    
    //don't attempt to send an empty string
    if (!message || message.length == 0) return false;
    
    //reset history to latest message sent
    this.sentMessageHistoryIndex = -1;
    
    this.sentMessages.unshift(message);
    this.fireEvent('send-message', message);
    
    this.msgInput.dom.value = '';
    return false; //stops the enter key creating new line on textarea
  },
  
  /**
   * Shows the previous stored history message if one is present
   */
  historyBack: function() {
    if (this.showHistoryMessage(this.sentMessageHistoryIndex + 1)) {
      this.sentMessageHistoryIndex += 1;
    };
  },
  
  /**
   * Shows the next stored history message if one is present
   */
  historyForward: function() {
    if (this.showHistoryMessage(this.sentMessageHistoryIndex - 1)) {
      this.sentMessageHistoryIndex -= 1;
    };
  },
  
  /**
   * Displays a previously sent message from the message history
   * @param {Number} index The index of the sentMessages history array to display
   * @return {Boolean} True if history message successfully shown, false otherwise
   */
  showHistoryMessage: function(index) {
    var val = this.sentMessages[index];
    
    if (val) {
      this.msgInput.dom.value = val;
      return true;
    };
    
    return false;
  }
});

Ext.reg('chat_panel', Kohive.views.chat.ChatPanel);

/**
 * @class Kohive.views.chat.HistoryPanel
 * @extends Ext.Panel
 * Displays a chat history
 */
Kohive.views.chat.HistoryPanel = Ext.extend(Ext.Panel, {
  
  /**
   * @property urlRegex
   * @type RegExp
   * A regex to match urls in the text to allow them to be highlighted
   */
  urlRegex: new RegExp("(http:\/\/[www\.]*[a-zA-Z0-9\-\_]*\.[a-z]{2,3}[\.a-z]{3,4}[\-\/\?\=\+\.\_a-zA-Z0-9]*)", 'g'),
  
  /**
   * @property channelRegex
   * @type RegExp
   * A regex to match channel names in the text to allow them to be highlighted
   */
  channelRegex: new RegExp("(#[A-Za-z0-9]*)"),
  
  /**
   * @property autoOpenLinks
   * @type Boolean
   * If true, automatically attach an event listener to open a new window when a url is clicked in the history messages
   * (defaults to true)
   */
  autoOpenLinks: true,
  
  /**
   * @property numberOfNicknameClasses
   * @type Number
   * The number of nickname CSS classes to cycle through (e.g. 'nickname-1' up to 'nickname-x').  CSS classes can target
   * each to provide different styling for each nickname
   */
  numberOfNicknameClasses: 3,
  
  /**
   * @property emoticons
   * @type Array
   * Emoticon mappings
   */
  emoticons: [
    { key: '>:-(',  value: 'Baring Teeth' },
    { key: ':-$',   value: 'embarrassed' },
    { key: ':-f',   value: 'Foot In Mouth' },
    { key: 'brb',   value: 'Be right back' },
    { key: ":'(",   value: 'crying.gif' },
    { key: ':-*',   value: 'Sick' },
    { key: '(y)',   value: 'Thumbs Up' },
    { key: ':-x',   value: 'Kiss' },
    { key: '8-)',   value: 'Nerd' },
    { key: ':-z',   value: "Dont Tell Anyone" },
    { key: ':-(',   value: 'Sad' },
    { key: '(6)',   value: 'Devil' },
    { key: '>:-)',  value: 'Devil' },
    { key: '(n)',   value: 'Thumbs Down' },
    { key: ':-D',   value: 'Grin' },
    { key: ':-|',   value: 'Disappointed' },
    { key: '<:-)',  value: 'Party' },
    { key: ':-\\',  value: "I Dont Know" },
    { key: ':-/',   value: "I Dont Know" },
    { key: ';-)',   value: 'wink' },
    { key: ';-\\',  value: 'Sarcastic' },
    { key: 'o:-)',  value: 'Angel' },
    { key: '0:-)',  value: 'Angel' },
    { key: ':-S',   value: 'Confused' },
    { key: ':-)',   value: 'Smile' },
    { key: '%|',    value: 'Eye-Rolling.gif' },
    { key: ':-o',   value: 'Surprised' },
    { key: ':-@',   value: 'Angry' },
    { key: ':-P',   value: 'tongue' }
  ],
  
  constructor: function(config) {
    config = config || {};
    
    Ext.applyIf(config, {
      ulId: 'irc-chat-history-messages-' + new Date().format('is')
    });
    
    Kohive.views.chat.HistoryPanel.superclass.constructor.apply(this, arguments);
  },

  /**
   * Initialise everything, add our events and listeners
   */
  initComponent: function() {
    Ext.applyIf(this, {
      cls:       'irc-chat-history',
      nicknames: [],
      messages:  [],
      html: {
        tag:  'ul',
        id:   this.ulId,
				cls: 'irc-chat-history-wrapper'
      },
      
      /**
       * @property currentChannel
       * @type Ext.ux.IRC.Channel
       * The channel this History panel is currently listening to
       */
      currentChannel: null
    });
    
    Kohive.views.chat.HistoryPanel.superclass.initComponent.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event channel-clicked
       * Fired when a link to a channel name is clicked in the history panel
       * @param {String} channelName The name of the channel that was clicked (e.g. '#extjs')
       */
      'channel-clicked',
      
      /**
       * @event link-clicked
       * Fired when a link to a website url is clicked in the history panel
       * @param {String} url The url clicked
       */
      'link-clicked'
    );
    
    if (this.autoOpenLinks) {
      this.on('link-clicked', window.open, window);
    };
    
    //Attach click handler to open links in text
    this.on('render', function() {
      this.el.on('click', function(e) {
        var t;
        if (t  = e.getTarget('span.link')) {
          this.fireEvent('link-clicked', Ext.get(t).dom.innerHTML);
        }
        else if (t = e.getTarget('span.channel')) {
          this.fireEvent('channel-clicked', Ext.get(t).dom.innerHTML);
        }
			  else {
			 		var f = Ext.getBody().child('.irc-chat-message-input-wrapper input');
					if (f) f.focus();
			  }
      }, this);
    }, this);
  },
  
  /**
   * Updates which channel this listens to, removes all current messages and adds any already in the channel
   * @param {Ext.ux.IRC.Channel} channel The channel to switch to
   */
  setCurrentChannel: function(channel) {
    //listen to any new messages on this channel, remove listener from previous channel
    if (this.currentChannel != undefined) {
      this.currentChannel.un('message-received', this.addMessage, this);
    }
    
    this.currentChannel = channel;
    this.currentChannel.on('message-received', this.addMessage, this);
    
    //remove existing history
    var ul = Ext.get(this.ulId);
    ul.dom.innerHTML = '';
    this.nicknames = [];
    
    //add any messages we know about in this channel
    Ext.each(channel.messages, function(message) {
      this.addMessage(message.member, message);
    }, this);
  },
  
  // From Prototype JS RegExp.escape
  regExpEscape: function(string) {
    return String(string).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
  },
  
  addMessage: function(member, message) {
    message.created_at = message.created_at || new Date();
    
    var kohiveUserName = Kohive.OS.getController('chat').views.index.prototype.ircUserNameToKohiveUserName(member.nickname);
    this.addNickname(kohiveUserName);
    
    //match urls
    var messageMarkup = message.text.replace(this.urlRegex, "<span class='link'>$1</span>");
    
    //match channels
    messageMarkup = messageMarkup.replace(this.channelRegex, "<span class='channel'>$1</span>");
    
    // Add emoticons
    var emoticonSrc, emoticonImage, dashRegExp = /\-/;
    Ext.each(this.emoticons, function (emoticon) {
			//check if it is :-/
			if (emoticon.key == ":-/" && messageMarkup != ":-/" && messageMarkup != ":/") var addSpace = true;
			
      // Default is .png, some are .gif though
      emoticonSrc = /\.gif/.test(emoticon.value) ? emoticon.value : emoticon.value + '.png';
      emoticonImage = "<img src='/images/apps/Chat/emoticons/" + emoticonSrc + "'/>";
      
      // Replacing emoticon string with emoticon image, g = global, i = case-insensitive
      messageMarkup = messageMarkup.replace(new RegExp(this.regExpEscape((addSpace === true) ? " " + emoticon.key : emoticon.key), 'gi'), (addSpace === true) ? " " + emoticonImage : emoticonImage);
      
      // All the ones with a dash should also work without a dash
      if (dashRegExp.test(emoticon.key)) {
        messageMarkup = messageMarkup.replace(new RegExp(this.regExpEscape((addSpace === true) ? " " + emoticon.key.replace(dashRegExp, '') : emoticon.key.replace(dashRegExp, '')), 'gi'), (addSpace === true) ? " " + emoticonImage : emoticonImage);
      }
    }, this);
		
		//check if it is flipped (current author)
		var flipped = (Kohive.OS.getIdCardData().nickname == kohiveUserName) ? true : false;
		
		// Get the avatar
		var avatar = "/avatars/thumb/missing.png";
		
		Ext.each(Kohive.OS.getCurrentHiveMemberships() || [], function(m) {
      if (kohiveUserName == m.data.id_card.nickname) {
				return avatar = m.data.id_card.thumb_filename;
      };
    }, this);
		
		var config = {
			id:       Ext.id(),
			message:  messageMarkup,
			nickname: kohiveUserName,
			avatar:   avatar,
			time:     message.created_at.format("H:i"),
			flipped:  flipped
		};
		
		this.template.append(this.ulId, config, true);
		
		var el = Ext.get(config.id);
		
		el.on('mouseover', el.addClass.createDelegate(el, ['x-over']), this);
		el.on('mouseout',  el.removeClass.createDelegate(el, ['x-over']), this);
    
    this.scrollToBottom(); 
    this.fireEvent('historyUpdated', this);
  },
  
  /**
   * Scrolls to the bottom of the chat UL
   */
  scrollToBottom: function() {
    //FIXME: For some reason scroll isn't working properly as of 3.0RC1 so this is hacked instead
		// TODO why does the animate not work?
    Ext.get(this.ulId).parent().scrollTo('top', 900000);
  },
  
  addNickname: function(nickname) {
    if (this.nicknames.indexOf(nickname) == -1) {
      this.nicknames.push(nickname);
    }
  },
  
  getNicknameIndex: function(nickname) {
    return this.nicknames.indexOf(nickname) % this.numberOfNicknameClasses + 1;
  },

	template: new Ext.XTemplate(
		'<div id="{id}" class="chat-bubble<tpl if="flipped"> flipped</tpl>">',
			'<div class="wrapper">',
				'<div class="l">',
					'<div class="r">',
						'<div class="tl">',
							'<div class="tr">',
								'<div class="bg">',
									'<div class="t">',
										'<div class="content">{message}</div>',
										'<div class="b">',
											'<div class="bl"></div>',
											'<div class="br"></div>',
										'</div>',
									'</div>',
								'</div>',
							'</div>',
						'</div>',
					'</div>',
				'</div>',

				'<div class="timestamp">{time}</div>',
				
			'</div>',		
			'<div class="avatar"><img src="{avatar}" alt="Avatar - {nickname}" /></div>',
			'<div class="nickname">{nickname}</div>',
		'</div>',	
		'<br />'
	)
});

Ext.reg('history_panel', Kohive.views.chat.HistoryPanel);

/**
 * @class Kohive.views.chat.UsersPanel
 * @extends Ext.Panel
 * Represents the users in the current channel
 */
Kohive.views.chat.UsersPanel = Ext.extend(Ext.Panel, {
  
  /**
   * @property baseCls
   * @type String
   * The CSS class to add to the wrapper element (defaults to 'irc-channel-panel')
   */
  baseCls: 'irc-channel-panel',
  
  /**
   * @property membersUlCls
   * @type String
   * The CSS class to add to the <ul> elements which hold members in a given channel
   */
  membersUlCls: 'irc-members',
  
  /**
   * @property activeChannel
   * @type Ext.ux.IRC.Channel
   * The name of the currently active channel
   */
  activeChannel: '',
  
  /**
   * @property hiveChannelName
   * @type String
   * The name of the hive channel name our view instance belongs to, since it may receive events that
   * come from another channel
   */
  hiveChannelName: '',
  
  /**
   * @property rebuildEvents
   * @type Array
   * An array of events fired by the Ext.ux.IRC.Manager that will trigger an HTML rebuild of the channels <ul>
   */
  rebuildEvents:  ['channel-joined', 'channel-left'],
 
  initComponent: function() {
    Ext.applyIf(this, {
      html: {
        tag: 'ul', 
        cls: this.membersUlCls
      }
    });
    
    Ext.ux.IRC.ChannelPanel.superclass.initComponent.apply(this, arguments);
    
    this.initListeners();
    
    // saving the hive channel name this view belongs to, since it may receive events that are
    // from another channel (hive)
    this.hiveChannelName = Kohive.OS.getController('chat').currentHiveChannelName();
  },
  
  /**
   * Listens to IRC and OS events
   */
  initListeners: function() {
    if (this.manager) {
      this.manager.on({
        scope: this,
        'channel-joined': this.setActiveChannel
      });
      
      // TODO: these two listeners cause the rebuild of the users list of chats that are in a background hive
      // which doesn't work correctly, see http://www.pivotaltracker.com/story/show/882585
      this.manager.channels.on('member-joined', this.buildElements, this);
      this.manager.channels.on('member-left',   this.buildElements, this);
    }
  },
  
  /**
   * Marks the requested channel name as the active channel, and all others as inactive
   * @param {Ext.ux.IRC.Channel/String} channelName The channel to set active, or its string name
   */
  setActiveChannel: function(channel) {
    //normalise inputs to an Ext.ux.IRC.Channel object
    if (typeof channel == 'string') channel = this.manager.channels.get('#' + channel);
    
    if (channel.name != this.hiveChannelName)
      return true; // return true, otherwise the following event listeners don't receive the event by which this handler is called
    
    //record the newly active channel
    this.activeChannel = channel;
    
    //Rebuild the HTML and add the 'active' CSS class to the appropriate channel
    this.buildElements(this.activeChannel);
  },
  
  /**
   * Builds the channel and member listings based this.manager.  Removes any existing elements
   */
  buildElements: function(channel) {
    if (channel.name != this.hiveChannelName)
      return true; // return true, otherwise the following event listeners don't receive the event by which this handler is called
    
    //clear out existing elements
    var u = this.el.child('ul.' + this.membersUlCls);
    u.dom.innerHTML = '';
    
    if (this.activeChannel.members == undefined) return;
    
    this.activeChannel.members.each(function(member) {
      u.createChild({
        tag : 'li',
				cls : 'online',
        html: Kohive.OS.getController('chat').views.index.prototype.ircUserNameToKohiveUserName(member.nickname) // not sure how to access the index instance
      });
    }, this);
  }
});

Ext.reg('channel-panel', Ext.ux.IRC.ChannelPanel);

/**
 * @class Kohive.views.doodle.Index
 * @extends Kohive.Window 
 * Shows the doodle application
 */
Kohive.views.doodle.Index = Ext.extend(Kohive.Window, {
	
  constructor: function(config) {
		Ext.applyIf(config, {
      title:             Ext.ux.Translator.translate('app_doodle'),
	    url:               'doodle',
      autoScroll:        false,
	    hbar:              false,
			filterPanelConfig: false,
	    bbar:              false,
			minHeight:         400,
			items: [
				{
					id:   'doodle_loading_panel',
					html: Ext.ux.Translator.translate('app_statusbar_loading')
				}
			]
    });
    
    Kohive.views.doodle.Index.superclass.constructor.call(this, config);

		this.initListeners();
	},
	
	/**
   * Attach listeners to the view
   */
  initListeners: function() {
    this.on('show', this.setupDoodlePanel, this);
  },
	
	/**
	 * Sets up the doodle panel view and adds it into the doodle window.
	 */ 
	setupDoodlePanel: function() {
		var doodleUrl = this.buildDoodlePanelUrl();
		
		// Check if the doodle url exists
		if (!doodleUrl) { return Ext.Msg.alert(Ext.ux.Translator.translate('error'), Ext.ux.Translator.translate('something_wrong')); };
		
		// Generate the doodle panel
		var doodlePanel = this.buildDoodlePanel(doodleUrl);
		
		// Remove all previous items from the application window
		this.remove('doodle_loading_panel');
		this.remove('doodle_flash_panel');
		
		// Add the new doodle panel into the application window
		this.add(doodlePanel);
		this.doLayout();
	},
	
	/**
	 * Builds a doodle panel with the proper Url for the current hive
	 * @param {String} swfUrl The Swf url for doodle application
	 */
	buildDoodlePanel: function(swfUrl) {
		var doodlePanel = new Ext.ux.FlashPanel({
			id:         'doodle_flash_panel',
	    border:     true,
	    autoScroll: false,
			bodyStyle:  'padding:1px',
	    mediaCfg:   {
	      url: swfUrl
	    }
	  });
	
		return doodlePanel;
	},
	
	/**
	 * Builds and returns the Doodle Swf Url for the current hive
	 */
	buildDoodlePanelUrl: function() {
	  var os   = ExtMVC.OS.getOS();
	  var hive = os.getCurrentHive();
		
		if (hive && hive.get('hive')) { return '/flash/doodle.swf?hiveId=' + hive.get('hive').id; };
		
		return null;
	}
});

Ext.reg('kohive_app_doodle_view_index', Kohive.views.doodle.Index);

/**
 * @class Kohive.views.filecabinet.Index
 * @extends Kohive.Window
 * The main file cabinet application window
 */
Kohive.views.filecabinet.Index = Ext.extend(Kohive.Window, {
  title:      Ext.ux.Translator.translate('app_file_cabinet'),
  cls:        'x-filecabinet-index',
  url:        'filecabinet',
  layout:     'border',
  width:      700,
  height:     400,
  defaults:   {border: false},
	toggledAll: false,
  
  /**
   * Creates the sub-view elements
   */
  initComponent: function() {
		// row expander
    var expander = new Ext.grid.RowExpander({
	    tpl:           new Ext.XTemplate(
	      '<tpl if="versions">',
	      '<div class="x-wrapper">',
	        '<div class="x-versions">',
	          '<table cellpaddng="0" cellspacing="0" width="97%">',
	            '<tpl for="versions">',
	            '<tr>',
	              '<td width="26%">' + Ext.ux.Translator.translate('version') + ' {num}<div class="x-down"></div></td>',
	              '<td width="26%">{created_at}</td>',
	              '<td>{author_nickname}</td>',
	              '<td align="right">{file_file_size}</td>',
	            '</tr>',
	            '</tpl>',
	          '</table>',
  	      '</div>',
	      '</div>',
	      '</tpl>',
	      
	      '<tpl if="description || tags || can_edit">',
	      '<div class="x-wrapper">',
  				'<tpl if="description">',
  					'<div class="x-description"><div class="action-edit-description">{description_html}</div></div>',
  				'</tpl>',
  				'<tpl if="!description && can_edit">',
  					'<div class="x-description x-empty"><div class="action-edit-description">' + Ext.ux.Translator.translate('app_add_description') + '</div></div>',
  				'</tpl>',
       
          '<tpl if="tags">',
            '<div class="x-tags<tpl if="description || can_edit"> can_edit</tpl>">',
              '<div class="action-editTags-tags">',
                '<div class="x-form-field-wrap x-trigger-field-wrap"><div class="x-form-text x-superboxselect  x-superboxselect-display-btns">',
                  '<ul>',
                    '<tpl for="tags">',
                      '<li class="x-superboxselect-item no-close">{name}</li>',
                      '</tpl>',
                    '</ul>',
                  '</div>',
                '</div>',
              '</div>',
            '</div>',
          '</tpl>',
          '<tpl if="!tags && can_edit">',
            '<div class="x-tags x-empty"><div class="action-editTags-tags">' + Ext.ux.Translator.translate('app_add_tags') + '</div></div>',
          '</tpl>',
        '</div>',
        '</tpl>'
	    )
    });
		
    this.dataGrid = new Ext.grid.GridPanel({
			id:    'filecabinet-grid-' + Kohive.OS.getCurrentHiveId(),
			store: this.store,
			
			plugins:     expander,
      collapsible: true,
			
			cm: new Ext.grid.ColumnModel([
				expander,
				{
					header:       Ext.ux.Translator.translate('name'),
					dataIndex:    'title',
					title:        'files_title',
					sortable:     true,
					menuDisabled: true,
					renderer: function(v, params, record) {
						var isDoc = ['doc', 'html', 'pdf', 'sxw', 'odt', 'rtf', 'txt'].indexOf(record.get('extension')) != -1;
						var isXls = ['xls', 'sxc', 'csv'].indexOf(record.get('extension')) != -1;
						var isPpt = ['ppt', 'pps'].indexOf(record.get('extension')) != -1;
						
						var res = '<div class="action-edit-title">' + record.get('title') + '</div>';
						// Maybe generate the form on the fly in the future
						if (isDoc || isXls || isPpt) {
              var zoho_url;
              if (isDoc)
                zoho_url = 'https://export.writer.zoho.com/remotedoc.im';
              else if (isXls)
                zoho_url = 'https://sheet.zoho.com/remotedoc.im';
              else if (isPpt)
                zoho_url = 'https://show.zoho.com/remotedoc.im';
              
              res += '<form action="' + zoho_url + '?apikey=43a3fc5746107ef383ae7529b834219e&output=editor" method="POST" id="zohoform' + record.data.id + '" target="_blank">';
              res += '<input type="hidden" name="url" value="' + record.get('url') + '">';
              if (isDoc || isXls)
                res += '<input type="hidden" name="saveurl" value="https://www.kohive.com/zoho/update">';
              res += '<input type="hidden" name="filename" value="' + record.get('file_file_name') + '">';
              res += '<input type="hidden" name="id" value="' + record.get('id') + '-' + Kohive.OS.getCurrentMembership().get('zoho_auth_token') + '">';
              res += '<input type="hidden" name="format" value="' + record.get('extension') + '">';
              res += '</form>';
						}
						if (isDoc || isXls) res += '<div class="x-edit" id="' + Ext.id() + '"></div>';
						if (isPpt) res += '<div class="x-view" id="' + Ext.id() + '"></div>';
						res = res + '<div class="x-download" id="' + Ext.id() + '"></div>';
						
						return res;
					}
				},
				{
					header:       Ext.ux.Translator.translate('date'),
					renderer:     Ext.util.Format.dateRenderer('j-M-y'),
					dataIndex:    'created_at_date',
					sortable:     true,
					menuDisabled: true
				},
				{
					header:       Ext.ux.Translator.translate('author'),
					dataIndex:    'author_nickname',
					sortable:     true,
					menuDisabled: true
				},
				{
					header:       Ext.ux.Translator.translate('size'),
					dataIndex:    'file_file_size',
					sortable:     true,
					align:        'right',
					renderer: function(v, params, record) {
						var value = record.get('file_file_size');
						var newValue = 0;
						var type = "mb";
					
						// Try converting it to MB first
						newValue = Math.round(value / 1048576 * 100) / 100;
					
						// Check if it has a decimal..
						if (/^0.*/.test(newValue)) {
							newValue = Math.round(value / 1024);
							type = "kb";
						};
					
						newValue = newValue + " " + type;
						
						return newValue;
          },
					menuDisabled: true
				},
				{
					header:    '',
					dataIndex: 'id',
					width:     17,
					resizable: false,
					renderer: function(v, params, record) {
						if (record.get('can_delete')) return '<div class="x-delete" id="' + Ext.id() + '"></div>';
						
						return '';
					}
				}
			]),
			
			viewConfig: {
			  rowSelectorDepth: 20,
				forceFit:         true,
				scrollOffset:     10,
				emptyText:        '<div class="no-results">' + Ext.ux.Translator.translate('app_nothing_to_display') + '</div>'
			},
			
			sortInfo: {
				field:     'title',
				direction: 'ASC'
			},
			
			autoExpandColumn: 'files_title',
			
			// stateEvents: ['sortchange', 'expand', 'collapse'],
			
			frame: true,
			
			listeners: {
				'rowclick': {
					scope: this,
					fn:    this.delegateRowClick
				}
	    },

	    keys: [
	    //   {
	    //     key:     'a',
	    //     scope:   this,
	    //     handler: this.onAdd
	    //   },
	    //   {
	    //     key:     'e',
	    //     scope:   this,
	    //     handler: this.onEdit
	    //   },
	      {
	        key:     Ext.EventObject.DELETE,
	        scope:   this,
	        handler: this.onDestroy
	      }
	    ]
		});
		
		this.downloadAllButton = new Ext.ux.HeaderToolbarButton({
      text:     Ext.ux.Translator.translate('download_all'),
			id:       'download_files_button',
      iconCls:  'download',
      handler:  this.onDownload,
      scope:    this
    });
		
		// Listen to the expand + collapse events on the expand plugin, so we can save state
		this.dataGrid.relayEvents(this.dataGrid.plugins, ['expand', 'collapse']);
		
    Ext.apply(this, {
      hbar:     false,
      bbar:     true,
      filterPanelConfig: {
        filters: [
          {
            ftype:        'store-helper',
            displayField: 'tags.name',
            valueField:   'tags.name',
            title:        Ext.ux.Translator.translate('tags'),
            iconCls:      'tag',
            view:         this,
            listenTo: {
              component: this.store,
              events:    ['load', 'update']
            }
          },
          {
            ftype:        'store-helper',
            displayField: 'author_nickname',
            valueField:   'author_membership_id',
            title:        Ext.ux.Translator.translate('authors'),
            iconCls:      'author',
            view:         this,
            listenTo: {
              component: this.store,
              events:    ['load', 'update']
            }
          },
          {
            ftype:        'store-helper',
            displayField: 'file_extension',
            valueField:   'file_extension',
            title:        Ext.ux.Translator.translate('type'),
            iconCls:      'extension',
            view:         this,
            listenTo: {
              component: this.store,
              events:    ['load', 'update']
            }
          }
        ],
        listeners: {
          scope            : this,
          'filters-updated': this.onFiltersUpdated
        }
      },
      id:     'filecabinet_index_' + Kohive.OS.getCurrentHiveId(),
			layout: 'fit',
			
			// This bad boy fixes horizontal scrolling issues in Safari only.
			autoScroll: false,
			
			plugins:    [new Ext.ux.HeaderToolbar()],
      hbar: [
        {
          text:     Ext.ux.Translator.translate('upload'),
          id:       'upload_file_button' + Kohive.OS.getCurrentHiveId(),
          iconCls:  'upload',
          xtype:    'header_toolbar_button',
					disabled: true,
          scope:    this
        },
        {
          text:     Ext.ux.Translator.translate('refresh'),
          id:       'refresh_files_button',
          iconCls:  'refresh',
          xtype:    'header_toolbar_button',
					disabled: true,
          handler: function() { 
            this.fireEvent('refresh');
          },
          scope:   this
        },
				{
	        text:     Ext.ux.Translator.translate('toggle_all'),
					id:       'toggle_files_button',
	        iconCls:  'toggle-all',
	        xtype:    'header_toolbar_button',
					disabled: true,
	        handler:  this.onToggleAll,
	        scope:    this
	      },
				'->',
				{
	        xtype: 'searchfield',
					listeners: {
						scope: this,
						'keyup' : this.searchStore
					}
	      },
				this.downloadAllButton
      ],
			
      items: [
				this.dataGrid
      ]
    });
    
    Kohive.views.filecabinet.Index.superclass.initComponent.apply(this, arguments);
    
    this.initListeners();
  },
	
  /**
   * Called whenever the user changes the filters applied in the filter bar. Fires the 'filter' event
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  onFiltersUpdated: function(filtersApplied) {
    this.fireEvent('filter', filtersApplied);
  },
  
  /**
   * Listen to events fired by the controller and sub-views
   */
  initListeners: function() {
    var controller = Kohive.OS.getController('filecabinet');
    
    controller.on({
      scope: this,
      // 'before-create-album' : this.showCreating,
      // 'album-created'       : this.onAlbumCreated,
      'update'              : this.onFileUpdated,
      // 'image-destroyed'     : this.showSuccess,
      'before-load-files'  : this.showRefreshing,
      'files-loaded'       : this.onFilesLoaded,
      'file-destroyed'     : this.onFileDestroyed
      // 'show-image'          : this.onShowImage
    });
    
    this.store.on('load', function(store) {
      this.onStoreLoad(store);
    }, this);
		
		this.on({
			scope          : this,
			'refresh'      : this.refreshFileStore,
			'destroy-file' : this.showDestroying
		});
		
		this.on('render', function() {
			this.downloadAllButton.hide();
		}, this);
		
		this.dataGrid.on('rowclick', this.updateDownloadAll, this);
  },
  
  onStoreLoad: function (store) {
    this.viewOnLoad(store);
    
    //update the original store if the URL hasn't got a ? in it (filtered)
    if (store.proxy.conn.url.split('?').length == 1) {
      this.originalStore = store.data.items;
    };
  },
  
  /**
   * Sets up all events emitted by this component.
   * This is called by the superclass so no need to call it from initComponent or constructor
   */
  initEvents: function() {
    Kohive.views.filecabinet.Index.superclass.initEvents.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event create-album
       * Fires when the user attempts to create a new album
       * This is relayed from the Viewer sub-view component
       */

      /**
       * @event create
       * Fires when the user attempts to upload (create) an image
       */
      'create',
      
      /**
       * @event refresh
       * Fires when the user clicks the refresh button
       */
      'refresh',
      
      /**
       * @event update-file
       * Fires when the user wishes to update an file
       * @param {Kohive.models.File} file The file to update
       * @param {Object} fields The fields object to update with
       */
      'update-file',
      
      /**
       * @event destroy-file
       * Fires when the user attempts to destroy an file
			 * @param {Ext.models.File} file The record of the file
       */
      'destroy-file',

			/**
			 * @event download-file
			 * Fires when the user attempts to download a file
			 * @param {Ext.models.File} file The record of the file
			 */
			'download-file',
			
			/**
			 * @event edit-file
			 * Fires when the user attempts to edit a file
			 * @param {Ext.models.File} file The record of the file
			 */
			'edit-file',
      
      /**
       * @event upload-started
       * Fires when a flash-powered upload begins
       */
      'upload-started',
      
      /**
       * @event upload-complete
       * Fires when a flash-powered upload is complete
       */
      'upload-complete',
      
      /**
       * @event update-filter-bar
       * Fires when the attached filter bar needs to update itself as a new album is being viewed
       * @param {ExtMVC.Model.Base} album The album that was just loaded
       */
      'update-filter-bar'
    );
  },
	
	/**
	 * @property errored
	 * @type Boolean
	 * True if there has been an error when uploading
	 */
	errored: false,
	
	uploading: false,
	
	/**
   * Called when the files-loaded event is fired by the controller
   * @param {Ext.data.Store} files The files to load
   */
  onFilesLoaded: function(images) {
		var obj = this;
		
		this.hideLoading();
		
    this.getSessionInfo(function(session) {
      //TODO: Getting the url like this relies on a hack used by the controller.  Again, this should be in model
      var url   = Kohive.OS.getController('filecabinet').scopedUrl('file_cabinets.json'),
          key   = session.get('_kohive_session'),
          token = session.get('auth_token'),
          win   = this; //needed for flash callbacks below
          
      // Check if the swf is already there, if it is, remove it
      if (currentUploader) currentUploader.remove();
      var currentQueue = this.el.child('.fileUploadQueue');
      if (currentQueue) currentQueue.remove();
      var currentUploader = Ext.get('upload_file_button' + Kohive.OS.getCurrentHiveId() + 'Uploader');
      if (currentUploader) currentUploader.remove();
      
      // Ext.getCmp('upload_file_button' + Kohive.OS.getCurrentHiveId()).enable();
			Ext.getCmp('refresh_files_button').enable();
			Ext.getCmp('toggle_files_button').enable();
			
      $('#upload_file_button' + Kohive.OS.getCurrentHiveId()).fileUpload({
        'uploader':       '/flash/uploader.swf',
        'script':         url,
        'scriptData':     { '_kohive_session': key, 'auth_token': token },
        'fileDataName':   'file[file]', // Extract correct name of upload field from form field
        //'scriptAccess': 'always', // Incomment this, if for some reason it doesn't work
        'multi':          true,
        'auto':           true,
        'width':          38,
        'height':         45,
        'wmode':          'transparent',
        // 'fileDesc':       'png, jpg',
        // 'fileExt':        '*.jpg;*.png;*.jpeg;*.gif;*.zip;*.rar;*.dmg;*.psd;*.fla;',
        'sizeLimit':      100242880,
        'simUploadLimit': 150,
				// TODO add some kinda of progress bar to the files application. for some reason this method
				//      only returns 100% when the file is complete, which is slightly useless. Probably have to be
				//      done some other way anyway...
				// 'onProgress':     function(event, queueId, fileObj, data) {
				// 	console.log(data.percentage);
				// },
				'onError':        function() {
					win.showFailure();
				},
				'onProgress':     function(e, queueId, fileObj, data) {
					win.addToProgresser(queueId, fileObj, data);
				},
        'onStart':        function() {
					win.uploading = true;
         	win.showLoading(Ext.ux.Translator.translate('app_statusbar_uploading'));
          win.fireEvent('upload-started');
					win.setupProgresser();
        },
				'onComplete':     function(e, queueId, fileObj, response, data) {
					// win.refreshFupileStore();
				},
        'onAllComplete':  function(event, data) {
          win.fireEvent('upload-complete');
          win.refreshFileStore();
					win.showSuccess();
					win.uploading = false;
					
					win.pb.fadeOut.defer(1500, win.pb);
					
					var tempTask = new Ext.util.DelayedTask(function() {
					  win.showSuccess();
					}, this);
					tempTask.delay(300);
        }
      });
    });
  },
	
	files: {},
	
	/**
	 * Sets up a progresser on the window. Only for testing purposes.
	 * place holder for the actual progress bar
	 */
	setupProgresser: function() {
		//update the files and the number of files being uploaded
		this.files     = {};
		
		//create the progress bar
		if (!this.pb) {
			var pbContainer = this.el.child('.x-statusbar');
			this.pb = pbContainer.createChild({
				cls: 'x-progress-bar',
				children: [
					{
						tag: 'div'
					}
				]
			});
		
			this.progressBar = this.pb.child('div');
			this.pb.hide();
		};
		
		this.pb.show();
		this.progressBar.setWidth(24);
		this.progressBar.update("1%");
	},
	
	/**
	 * Adds a file to the progresser, and if it is already there it updates its progress percentage
	 * @param {String} id The id of the file
	 * @param {Object} fileObj Object with details of the file
	 * @param {Object} data Data of the file, including percentage
	 */
	addToProgresser: function(id, fileObj, data) {
		//add/update the file to the loggggg
		this.files[id] = [fileObj, data.percentage];
		
		//update the progress bar
		this.updateProgressBar();
	},
	
	/**
	 * Updates the progress bar.
	 * - Calculates the total amount of files. 
	 */
	updateProgressBar: function() {
		var totalFiles      = 0,
				totalPercentage = 0;
		
		//loop through each of the files, get the total amount of files and the total percentage
		for (var file in this.files) {
			totalFiles++;
			totalPercentage = totalPercentage + this.files[file][1];
		};
		
		var actualPercentage = Math.round(totalPercentage / totalFiles);
		this.progressBar.setWidth(actualPercentage.toString() + "%");
		this.progressBar.update(actualPercentage.toString() + "%");
		
		if (actualPercentage == 100) {
			this.showLoading(Ext.ux.Translator.translate('app_statusbar_processing'));
			this.pb.fadeOut.defer(1000, this.pb);
		};
	},
  
  /**
   * @property sessionInfo
   * @type Kohive.models.Session|Null
   * A cached copy of the Session model instance.  Don't access directly, use getSessionInfo instead
   */
  sessionInfo: null,
  
  /**
   * Calls back with a Session model object. If the session has not already been loaded it is
   * loaded first
   * @param {Function} callback The function to call when the session data has been loaded
   * This will be called with a single argument - the Kohive.model.Session model object
   * @param {Function} scope The scope to call the callback in (optional)
   */
  getSessionInfo: function(callback, scope) {
    scope = scope || this;
    
    if (this.sessionInfo == null) {
      Kohive.models.Session.find(1, {
        url:     '/session/get',
        scope:   this,
        success: function(session) {
          this.sessionInfo = session;
          callback.call(scope, this.sessionInfo);
        }
      });
    } else callback.call(scope, this.sessionInfo);
  },
	
	/**
	 * Refreshes the file store in the grid
	 */
	refreshFileStore: function() {
		this.store.load();
	},
	
	/**
	 * Method which is called when a file is destroyed
	 */
	onFileDestroyed: function() {
		this.showRefreshing();
		
		if (this.store.totalLength > 1) this.store.reload();
	  else {
	    this.onFiltersUpdated({});
	  };
	},
	
	onFileUpdated: function(file, forceRefresh) {
		this.showSuccess();
		if (forceRefresh) this.refreshFileStore();
	},
	
	/**
   * Called whenever the user changes the filters applied in the filter bar. Fires the 'filter' event
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  onFiltersUpdated: function(filters) {
    // TODO do this the proper way, until then, do without events..
		if (!this.store) return;
		
		//for some reason passing the params option to findAll isn't working, so build url manually :/
    var url        = Kohive.OS.scopedUrl('file_cabinets'),
        conditions = [];
    
    //loop through each of the filters and add them to the conditions
    for (filter in filters) {
      if (filters[filter].length > 0) conditions.push(String.format(filter + "={0}", filters[filter].join(',')));
    };
		
		//update the stores url + reload the store
		if (conditions.length > 0) this.store.proxy.conn.url = url + '?' + conditions.join("&");
		else this.store.proxy.conn.url = url;
		
    this.store.load();
  },
	
	/**
	 * Method which is called when a row is clicked on the files grid. Checks
	 * if the clicked target has a class of 'x-delete' and then fires off an
	 * event if it is..
	 */
	delegateRowClick: function(grid, index, e) {
		var target = Ext.get(e.getTarget().id);
		if (!target) return;
		
		if (target.hasClass('x-delete')) return this.onDestroy();
		if (target.hasClass('x-download')) return this.onDownload();
		if (target.hasClass('x-edit')) return this.onEdit();
		if (target.hasClass('x-view')) return this.onEdit();
	},
	
	/**
	 * Method which is called when a record needs to be destroyed
	 */
	onDestroy: function(key, e) {
	  // onDestroy is also called by delegateRowClick, which doesn't pass on the e parameter
	  if (e) {
	    // Checking if the delete key is pressed within a textfield while editing
	    // It should not delete the file then
      var el = Ext.get(e.getTarget());
      if (el.hasClass('x-editable')) return;
    }
		
		// Get the selected records from the grid panel
		var selections = this.dataGrid.getSelectionModel().getSelections();
		
		// Check if there is more than one selected
		var multi = (selections.length > 1) ? true : false;
		
		// Show the alert dialog
		return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     (multi) ? Ext.ux.Translator.translate('destroy_files_desc') : Ext.ux.Translator.translate('destroy_file_desc'),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
					Ext.each(selections, function(record) {
						this.fireEvent('destroy-file', record);
					}, this);
        }
      }   
    });
	},
	
	/**
	 * Method which is called when a record needs to be downloaded
	 */
	onDownload: function(key, event) {
		// Get the selected records from the grid panel
		var selections = this.dataGrid.getSelectionModel().getSelections();
		
		// Check if there is more than one selected
		var multi = (selections.length > 1) ? true : false;
		
		Ext.each(selections, function(record) {
			this.fireEvent('download-file', record);
		}, this);
	},
	
	/**
	 * Method which is called when a record needs to be editted
	 */
	onEdit: function(key, event) {
		// Get the selected records from the grid panel
		var selections = this.dataGrid.getSelectionModel().getSelections();
		
		// Check if there is more than one selected
		var multi = (selections.length > 1) ? true : false;
		
		Ext.each(selections, function(record) {
		  Ext.get('zohoform' + record.data.id).dom.submit();
		}, this);
	},
	
	/**
	 * Called when the toggle button is press on the window header. Gets all the rows from
	 * the grid panel view and expands/hides them all
	 */
	onToggleAll: function() {
		var dg = this.dataGrid;
		
		// Get all the rows in the datagrid view
		var rows = dg.view.getRows();
		
		Ext.each(rows || [], function(row) {
			if (this.toggledAll) dg.plugins.expandRow(row);
			else dg.plugins.collapseRow(row);
		}, this);
		
		this.toggledAll = (this.toggledAll) ? false : true;
	},
	
	/**
	 * 
	 */
	updateDownloadAll: function(grid, index, e) {
		var selections = this.dataGrid.getSelectionModel().getSelections();
		
		// Check if there is more than one selected
		var multi = (selections.length > 1) ? true : false;
		
		if (multi) {
			//check if the button is already visible
			if (this.downloadAllButtonHidden === false) return;
			
			this.downloadAllButton.show();
			this.downloadAllButton.el.hide();
			this.downloadAllButton.el.fadeIn();
			this.downloadAllButtonHidden = false;
		} else {
			this.downloadAllButton.el.fadeOut({
				scope: this,
				callback: function() {
					this.downloadAllButton.el.show();
					this.downloadAllButton.hide();
					this.downloadAllButtonHidden = true;
				}
			});
		};
	},
	
	/**
   * Creates a new Ext.ux.Editable instance bound to the gridpanel
   */
  setupEditable: function() {
		if (!this.dataGrid) return;
		
    this.editable = new Ext.ux.Editable(this.dataGrid, {
			view:           this, // Hack so functions like reply() (message board) can access the window view
			model:          Kohive.models.File, // Hack so function like reply() will have the right scope
			editableEvents: ['rowclick', 'rowdblclick'],
      clickActions:   Kohive.models.File.prototype.clickActions
    });
		
    this.editable.on('update', this.onBeforeUpdate, this);
    this.relayEvents(this.editable, ['update']);
    
    this.showSuccess();
  },
  
  /**
	 *  Method called when the grid store has been loaded.
	 * @param {Ext.data.Store} store The store which has been loaded
	 */
	viewOnLoad: function(store) {
	  var dataGrid = this.dataGrid;
	  if (!dataGrid) return;
	  
	  var store = dataGrid.store;
	  store.each(function(record, i) {
	    var el = Ext.get(dataGrid.view.getRow(i));
	    
      if (record.get('can_edit')) {
        // editable rollovers
        var items  = el.select('div'),
            fields = [];

        Ext.each(items.elements || [], function(item) {
          var item = Ext.get(item);
          
          Ext.each(item.dom.className.split(' '), function(cls) {
            if (cls.split('-')[0] == "action") fields.push(item);
          }, this);
        }, this);

        Ext.each(fields, function(field) {
          field.on('mouseover', field.addClass.createDelegate(field, ['x-over']));
          field.on('mouseout',  field.removeClass.createDelegate(field, ['x-over']));
        }, this);
      };
    }, this);
	}
});

Ext.reg('kohive_app_filecabinet_view_index', Kohive.views.filecabinet.Index);

/**
 * @class Kohive.views.idcards.New
 * @extends Kohive.Window
 * Window allowing the user to enter a new Id Card
 */
Kohive.views.idcards.New = Ext.extend(Kohive.Window, {
  
  constructor: function(config) {
    config = config || {};

		
    /**
     * @property submitButton
     * @type Ext.Button
     * The submit button for the new idcard form
     */
    this.submitButton = new Kohive.SmallButton({
      color:    'green',
      text:     Ext.ux.Translator.translate('save'),
			disabled: true
    });
    
    /**
	   * @property formPanel
	   * @type Ext.FormPanel
	   * The form for creating a new id card
	   */
	  this.formPanel = new Ext.FormPanel({
	    autoScroll: true,
      fileUpload: true,
	    labelWidth: 80,
	    bodyStyle: {
	      padding: '5px'
	    },
	    defaults: {
	      xtype:           'textfield',
				maxLength:       225,
	      anchor:          '-20',
				validationEvent: false
	    },
	    items: [
	      {
	        name: 'id_card[name]',
	        fieldLabel: Ext.ux.Translator.translate('id_card_name'),
	        allowBlank: false
	      },
	      {
	        name: 'id_card[full_name]',
	        fieldLabel: Ext.ux.Translator.translate('full_name'),
	        allowBlank: false
	      },
	      {
	        name: 'id_card[nickname]',
	        fieldLabel: Ext.ux.Translator.translate('nickname'),
	        allowBlank: false
	      },
        {
          name      : 'id_card[avatar]',
          xtype     : 'fileuploadfield',
          fieldLabel: Ext.ux.Translator.translate('picture')
        },
	      {
	        name: 'id_card[email]',
	        fieldLabel: Ext.ux.Translator.translate('email'),
					vtype: 'email'
	      },
	      {
	        name: 'id_card[website]',
	        fieldLabel: Ext.ux.Translator.translate('website')
	      },
	      {
	        name: 'id_card[phone]',
	        fieldLabel: Ext.ux.Translator.translate('phone')
	      },
	      {
	        xtype:  'panel',
	        html:   '<hr />',
	        border: false
	      },
	      {
	        name: 'id_card[job_title]',
	        fieldLabel: Ext.ux.Translator.translate('job_title')
	      },
	      {
	        xtype: 'combo',
	        name:  'id_card[gender]',
	        store: new Ext.data.SimpleStore({
	            fields: ['gender', 'label'],
	            data : [
	              ['male',   Ext.ux.Translator.translate('male')],
	              ['female', Ext.ux.Translator.translate('female')]
	            ]
	        }),
	        selectOnFocus: true,
	        triggerAction: 'all',
	        displayField:  'label',
	        fieldLabel:    Ext.ux.Translator.translate('gender'),
	        typeAhead:     true,
	        emptyText:     '',
	        mode:          'local'
	      },
	      {
	        name: 'id_card[sexuality]',
	        fieldLabel: Ext.ux.Translator.translate('sexuality')
	      },
	      {
	        name: 'id_card[location]',
	        fieldLabel: Ext.ux.Translator.translate('location')
	      },
	      {
	        name: 'id_card[relationship]',
	        fieldLabel: Ext.ux.Translator.translate('relationship')
	      },
	      {
	        name:      'id_card[birthday]',
	        fieldLabel: Ext.ux.Translator.translate('birthday')
	      },
	      {
	        name: 'id_card[religion]',
	        fieldLabel: Ext.ux.Translator.translate('religion')
	      },
				{
	        name: 'id_card[diet]',
	        fieldLabel: Ext.ux.Translator.translate('diet')
	      },
	      {
	        name: 'id_card[political_view]',
	        fieldLabel: Ext.ux.Translator.translate('political_view')
	      },
	      {
	        name: 'id_card[heritage]',
	        fieldLabel: Ext.ux.Translator.translate('heritage')
	      },
	      {
	        xtype:  'panel',
	        html:   '<strong>' + Ext.ux.Translator.translate('favorites') + '</strong><hr />',
	        border: false
	      },
	      {
	        name: 'id_card[activites]',
	        fieldLabel: Ext.ux.Translator.translate('activites')
	      },
	      {
	        name: 'id_card[topics]',
	        fieldLabel: Ext.ux.Translator.translate('topics')
	      },
	      {
	        name: 'id_card[foods]',
	        fieldLabel: Ext.ux.Translator.translate('foods')
	      },
	      {
	        name: 'id_card[drinks]',
	        fieldLabel: Ext.ux.Translator.translate('drinks')
	      },
	      {
	        name: 'id_card[movies]',
	        fieldLabel: Ext.ux.Translator.translate('movies')
	      },
	      {
	        name: 'id_card[tv_shows]',
	        fieldLabel: Ext.ux.Translator.translate('tv_shows')
	      },
	      {
	        name: 'id_card[music]',
	        fieldLabel: Ext.ux.Translator.translate('music')
	      },
	      {
	        name: 'id_card[books]',
	        fieldLabel: Ext.ux.Translator.translate('books')
	      },
	      {
	        name: 'id_card[role_models]',
	        fieldLabel: Ext.ux.Translator.translate('role_models')
	      },
	      {
	        name: 'id_card[places]',
	        fieldLabel: Ext.ux.Translator.translate('places')
	      },
	      {
	        name: 'id_card[quotes]',
	        fieldLabel: Ext.ux.Translator.translate('quotes')
	      },
	      {
	        xtype:  'panel',
	        html:   '<hr />',
	        border: false
	      },
	      {
	        xtype:      'textarea',
	        name:       'id_card[bio]',
					maxLength:  5000,
	        fieldLabel: Ext.ux.Translator.translate('bio')
	      },
	      {
	        xtype:  'panel',
	        height: 10,
	        border: false
	      }
	    ],
	    buttons: [this.submitButton]
	  });
    
    Ext.applyIf(config, {
      title:       Ext.ux.Translator.translate('app_new_id_card'),
      layout:      'fit',
      id:          'id_card_new',
      hbar:        false,
      bbar:        false,
      width:       400,
      height:      450,
			maximizable: false,
      bbar:        true,
      filterPanelConfig: false,
      url:         'idcards/new',
      items:       [this.formPanel],

			keys: [
        {
          key :      Ext.EventObject.ENTER,
          scope:     this,
          handler:   function(key, e) {
            var target = e.getTarget();
            if (target.type == "text") {
              this.onSubmit();
            };
          }
        },
        {
          key:       Ext.EventObject.ENTER,
          ctrl:      true,
          scope:     this,
          stopEvent: true,
          handler:   this.onSubmit
        }
      ]
    });
    
    Kohive.views.idcards.New.superclass.constructor.call(this, config);
    
    this.initListeners();
  },

 
  /**
   * Called when the Save button is clicked.  Fires the 'save' event with the form data
   */
  onSubmit: function() {
		// check if the form is valid
		if (!this.formPanel.getForm().isValid()) return;
		
		this.showSubmitting();
		
		var url = String.format("/memberships/{0}/id_cards", Kohive.OS.getCurrentHive().get('id')),
		    con = Kohive.OS.getController('idcards');
		
		this.formPanel.getForm().submit({
		  isUpload: true,
		  url     : url,
		  success : function(form, action) {
		    var idCard = new Kohive.models.IdCard(form.getValues());
		    con.fireEvent('create', idCard);
		  },
		  failure : function(form, action) {
		    var idCard = new Kohive.models.IdCard(form.getValues());
		    con.fireEvent('create-failed', idCard);
		  }
		});
  },
  
  /**
   * Called when the Id Card creation was successful.  Clears the form and hides this window
   * @param {Kohive.models.IdCard} idCard The new Id Card
   */
  onCreate: function(idCard) {
		this.showSuccess();
		this.formPanel.getForm().reset();
    this.hide.defer(200, this);
  },
  
  /**
   * Called when the Id Card creation was not successful.  Shows errors on the form
   * @param {Kohive.models.IdCard} idCard The Id Card that could not be successfully saved
   */
  onCreateFailed: function(idCard) {
    this.showFailure();
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event save
       * Fired when the user clicks the save button to create this Id Card
       * @param {Object} data The data the user entered into the form
       * @param {Ext.form.BasicForm} form The BasicForm wrapping the template's form
       */
      'save'
    );
    
    Kohive.views.idcards.New.superclass.initEvents.apply(this, arguments);
  },
  
  /**
   * Listens to events on the Id Cards controller
   */
  initListeners: function() {
    Kohive.OS.getController('idcards').on({
      scope          : this,
      'create'       : this.onCreate,
      'create-failed': this.onCreateFailed
    });
    
		this.on('show',   this.resetFormPanel,   this);
		this.on('render', this.listenToChildren, this);
		this.on('render', this.focusFirstItem,   this);
    this.submitButton.on('click', this.onSubmit, this);
  },

	/**
	 * Resets the form panel so there is no longer any content or form validation errors
	 */
	resetFormPanel: function() {
		var form = this.formPanel.getForm();
		form.reset();
	},

	/**
	 * Listens to each of the items in the formPanel and changes if they were changed, if they are then
	 * enable/disable the form submit button.
	 */
	listenToChildren: function() {
    Ext.each(this.formPanel.find(), function(field) {
      field.on('change', function() {
        // Check if the form is valid
        var f = this.formPanel.getForm();
        if (f.isValid()) {
          this.submitButton.enable();
        } else {
          this.submitButton.disable();
        };
      }, this);
    }, this);
  },
	
	/**
	 * Focus' on the first item of the form
	 */
	focusFirstItem: function() {
		var firstItem = this.formPanel.find()[0];
		
		if (firstItem) {
		 var tempTask = new Ext.util.DelayedTask(function() {
	      firstItem.focus();
	    }, this);
	    tempTask.delay(2000);
		};
	},
  
  //we have to override these to keep superclass happy :/
  getDataViewConfig: Ext.emptyFn,
  getDefaultHBar:    Ext.emptyFn
});

Ext.reg('kohive_app_idcards_view_new', Kohive.views.idcards.New);

/**
 * @class Kohive.views.idcards.Show
 * @extends Kohive.Window
 * @cfg {Kohive.models.idCard} idCard The ID card to display.  If this is not passed 
 * an error message will be displayed.
 * Shows a user's id card
 */
Kohive.views.idcards.Show = Ext.extend(Kohive.Window, {
  /**
   * @event update
   * A relayed event from this component's DataView signfying that an Id Card should be updated
   * @param {Kohive.models.IdCard} idCard The Id Card to update
   * @param {Object} updates The fields and values to update
   */
  
  closeAction: 'hide',
  
  constructor: function(config) {
    /**
     * @property store
     * @type Ext.data.Store
     * A fake store containing the single IdCard record
     */
    this.store = Kohive.models.IdCard.findAll({autoLoad: false});
    this.store.add([config.idCard]);
		
		this.idCard = config.idCard;
    
    /**
     * @property dataview
     * @type Ext.DataView
     * The DataView which renders the fake form
     */
    this.dataview = new Ext.DataView({
      xtype:        'dataview',
      emptyText:    'Could not find ID Card',
      autoHeight:   true,
      itemSelector: 'div.x-idcard-wrap',
      store:        this.store,
      tpl:          this.template
    });
    
    Ext.applyIf(config, {
      title:             this.getWindowTitle(config.idCard),
      layout:            'fit',
      id:                'id_cards_show_' + (config.idCard ? config.idCard.get('id') : 'unknown'),
      cls:               'x-idcard',
      hbar:              false,
      bbar:              true,
			width:             500,
      resizable:         true,
			resizeHandles:     's',
			stateful:          false,
			filterPanelConfig: null,
      maximizable:       false,
      url:               'idcards/show/' + (config.idCard ? config.idCard.get('id') : 'unknown'),
      items: [
				{
					border: true,
					items: [this.dataview]
				}
			]
    });
    
    Kohive.views.idcards.Show.superclass.constructor.call(this, config);
    
    this.initListeners();
  },
  
  /**
   * Attach listeners to the id cards controller
   */
  initListeners: function() {
    Kohive.OS.getController('idcards').on({
      scope            : this,
      'update'         : this.onUpdate,
      'update-failed'  : this.onUpdateFail,
      'verify-success' : this.showSuccess,
      'verify-failed'  : this.showFailure
    });
    
		this.on('render', function() {
			if (!this.idCard.get('verified_email') && this.idCard.get('email')) this.initStartPolling();
		}, this);
    this.on('render', this.setupEditable,      this);
    this.on('render', this.setupFlashUploader, this);
    this.dataview.on('render', this.initRollovers, this);
		this.dataview.on({
      scope: this,
      click: function(dataview, index, node, e) {
        var idCard = dataview.getRecord(node),
            el     = Ext.get(e.getTarget());
        
        if (el.hasClass('false') && el.hasClass('cursor')) this.onVerifyClick(idCard);
      }
    });
  },
	
	onVerifyClick: function(idCard) {
    return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('verify_id_card_email'),
      buttons: {no:Ext.ux.Translator.translate('verify_again'), yes:Ext.ux.Translator.translate('cancel')},
      fn:      function(msg) {
        if (msg == 'no') {
					var controller = Kohive.OS.getController('idcards');
					controller.verify(idCard);
					
					this.showSubmitting();
					
					this.initStartPolling();
        }
      }   
    });
  },
	
	initStartPolling: function() {
		if (this.pollingStarted) return;
		
		this.pollingStarted = true;
		
		var task = {
      run: function() {
        Kohive.models.IdCard.findById(this.idCard.get('id'), {
		      scope: this,
					url:   Kohive.OS.getController('idcards').scopedUrl(String.format("id_cards/{0}", this.idCard.get('id'))),
		      success: function(idCard) {
		        this.updateShowView(idCard);
						this.pollingStarted = false;
		      }
		    });
      },
      scope:    this,
      interval: 5000 // 5 seconds
    };
    var runner = new Ext.util.TaskRunner();
    runner.start(task);
	},
	
	updateShowView: function(idCard) {
		if (idCard.get('verified_email') === true) {
			Kohive.OS.getController('idcards').fireEvent('hive-id-card-update', this);
			this.store.getAt(0).set('verified_email', true);
		};
	},
  
  /**
   * Called when the Id Card has been successfully updated
   * @param {Kohive.models.IdCard} idCard The Id Card that was updated
   */
  onUpdate: function(idCard, updates, forceRefresh) {
    if (idCard == this.idCard) {
			this.setupFlashUploader();
			
			if (updates.email) {
				this.store.getAt(0).set('verified_email', false);
				this.initStartPolling();
			};
			
			if (!forceRefresh) {
				this.showSuccess();
			} else {
				this.showRefreshing();
				
				var scope = this;
				
				Kohive.models.IdCard.findById(idCard.get('id'), {
					url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/id_cards/' + idCard.get('id'),
					success: function(newIdCard) {
						idCard.set('birthday_date', newIdCard.get('birthday_date'));
						idCard.set('bio_html', newIdCard.get('bio_html'));
						scope.showSuccess();
					},
					failure: function() {
						scope.showFailure();
					}
				});
			};
    } else {
			this.hideLoading();
		};
  },
  
  /**
   * Called when an Id Card could not be saved
   * @param {Kohive.models.IdCard} idCard The Id Card that couldn't be saved
   */
  onUpdateFail: function(idCard) {
    this.showFailure();
  },
  
  /**
   * Called before the id card is updated
   */
  onBeforeUpdate: function() {
    this.showUpdating();
  },

	initRollovers: function() {
	  var tempTask = new Ext.util.DelayedTask(function() {
	    var items  = this.dataview.el.select('.x-value');
			
			if (this.idCard && this.idCard.get('can_edit')) {
				Ext.each(items.elements, function(item) {
					var item = Ext.get(item);
					
					item.on('mouseover', function() {
					  item.addClass('x-over');
					  if (!item.hasClass('x-empty')) this.fireEvent('statusbar-event-start', Ext.ux.Translator.translate('app_statusbar_double_click_to_edit'));
					}, this);
					item.on('mouseout', function() {
					  item.removeClass('x-over');
					  this.fireEvent('statusbar-event-stop');
					}, this);
				}, this);
			};
	  }, this);
	  tempTask.delay(1000);
	},
  
  /**
   * Sets up the Ext.ux.Editable plugin for the dataview
   */
  setupEditable: function() {
    this.editable = new Ext.ux.Editable(this.dataview, {
			model: 				Kohive.models.IdCard,
      clickActions: Kohive.models.IdCard.prototype.clickActions
    });
    
    this.editable.on('update', this.onBeforeUpdate, this);
    this.relayEvents(this.editable, ['update']);
    
    this.hideLoading();
  },
  
  /**
   * Creates a title for this window based on the Id Card
   * @param {Kohive.models.IdCard} idCard The Id card to base the window name off
   * @return {String} A meaningful window title
   */
  getWindowTitle: function(idCard) {
    var title = Ext.ux.Translator.translate('id_card');
    
    if (idCard.data.can_edit) title += ' : ' + (idCard ? idCard.get('name') : 'Unknown');
    
    return title;
  },

	/**
	 * Sets up the flash uploader and positions it in the correct place (over the current image)
	 */
	setupFlashUploader: function() {
		if (!this.idCard || !this.idCard.get('can_edit')) return;
		
		this.getSessionInfo(function(session) {
      //TODO: Getting the url like this relies on a hack used by the controller.  Again, this should be in model
      var url   = Kohive.OS.getController('idcards').scopedUrl(String.format("id_cards/{0}", this.initialConfig.idCard.get('id'))) + '/update_from_flash',
          key   = session.get('_kohive_session'),
          token = session.get('auth_token'),
          win   = this; //needed for flash callbacks below
      
			// Create the fake element the same size and position as the avatar
			var img   = this.el.child('img');
			var faker = img.parent().createChild({
				id:  'fake_avatar_element',
				cls: 'fake_avatar_element'
			});
			
      // Check if the swf is already there, if it is, remove it
      var currentUploader = Ext.get('fake_avatar_elementUploader');
      if (currentUploader) currentUploader.remove();
      
      $('#fake_avatar_element').fileUpload({
        'uploader':       '/flash/avatar-uploader.swf',
        'script':         url,
        'scriptData':     { '_kohive_session': key, 'auth_token': token },
        'fileDataName':   'id_card[avatar]', // Extract correct name of upload field from form field
        //'scriptAccess': 'always', // Incomment this, if for some reason it doesn't work
        'multi':          false,
        'auto':           true,
        'width':          145,
        'height':         215,
        'wmode':          'transparent',
        'fileDesc':       'png, jpg',
        'fileExt':        '*.jpg;*.png;*.jpeg;*.gif;',
        'sizeLimit':      5242880,
        'simUploadLimit': 150,
        'onStart':        function() {
          win.fireEvent('upload-started');
          win.showLoading(Ext.ux.Translator.translate('app_statusbar_uploading'));
        },
        'onAllComplete':  function(event, data) {
          win.showRefreshing();
          win.fireEvent('upload-complete');
					
					// Load the id card again
					var id = win.initialConfig.idCard.get('id');
					Kohive.models.IdCard.findById(id, {
			      scope: this,
						url:   Kohive.OS.getController('idcards').scopedUrl(String.format("id_cards/{0}", id)),
			      success: function(idCard) {
							win.store.removeAll();
							win.store.add([idCard]);
			        win.showSuccess();
							win.setupFlashUploader();
							
							var members = Kohive.OS.getController('members');
							members.fireAction('loadMembers', this, [Kohive.OS.getMemberPanel(), true]);
			      },
			      failure: function() {
			        win.showFailure();
			      }
			    });
					
          // win.fireEvent('refresh');
        }
      });
    });
	},
	
	/**
   * @property sessionInfo
   * @type Kohive.models.Session|Null
   * A cached copy of the Session model instance.  Don't access directly, use getSessionInfo instead
   */
  sessionInfo: null,
  
  /**
   * Calls back with a Session model object. If the session has not already been loaded it is
   * loaded first
   * @param {Function} callback The function to call when the session data has been loaded
   * This will be called with a single argument - the Kohive.model.Session model object
   * @param {Function} scope The scope to call the callback in (optional)
   */
  getSessionInfo: function(callback, scope) {
    scope = scope || this;
    
    if (this.sessionInfo == null) {
      Kohive.models.Session.find(1, {
        url:     '/session/get',
        scope:   this,
        success: function(session) {
          this.sessionInfo = session;
          callback.call(scope, this.sessionInfo);
        }
      });
    } else callback.call(scope, this.sessionInfo);
  },
  
  /**
   * @property template
   * @type Ext.XTemplate
   * The default template to generate panel markup for this ID Card from
   */
  template: new Ext.XTemplate(
    '<tpl for=".">',
      '<div class="x-idcard-wrap">',
         '<div class="top left">',
           '<div class="right">',
             '<div class="center">',
               '<div class="x-image">',
                 '<img class="action-uploadAvatar-avatar" src="{large_filename}" />',
               '</div>',
               '<div class="x-info">',
									
									{
										isCurrentUser: function(id) {
											return Kohive.OS.getCurrentUser().get('id') == id;
										}
									},
									
                  '<div class="x-item title">',
										'<tpl if="this.isCurrentUser(user_id)">',
                    '<span class="<tpl if="!name">x-empty </tpl>x-value{[values["is_default?"] ? "" : " action-edit-name"]}">',
										'<tpl if="!name">' + Ext.ux.Translator.translate('app_add') + '</tpl>',
										'<tpl if="name">{name}</tpl>',
										'</tpl>',
									'</span>',
                 '</div>',
								
								//full_name
                '<tpl if="full_name || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('full_name') + ':</td>',
										'<td>',
											'<span class="<tpl if="!full_name">x-empty </tpl>x-value action-edit-full_name<tpl if="!full_name"> x-empty</tpl>">',
												'<tpl if="!full_name">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="full_name">{full_name}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//nickname
                '<tpl if="nickname || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('nickname') + ':</td>',
										'<td>',
											'<span class="<tpl if="!nickname">x-empty </tpl>x-value action-edit-nickname<tpl if="!nickname"> x-empty</tpl>">',
												'<tpl if="!nickname">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="nickname">{nickname}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//job_title
								'<tpl if="job_title || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('job_title') + ':</td>',
										'<td>',
											'<span class="<tpl if="!job_title">x-empty </tpl>x-value action-edit-job_title<tpl if="!job_title"> x-empty</tpl>">',
												'<tpl if="!job_title">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="job_title">{job_title}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//location
								'<tpl if="location || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('location') + ':</td>',
										'<td>',
											'<span class="<tpl if="!location">x-empty </tpl>x-value action-edit-location<tpl if="!location"> x-empty</tpl>">',
												'<tpl if="!location">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="location">{location}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//gender
								'<tpl if="gender || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('gender') + ':</td>',
										'<td>',
											'<span class="<tpl if="!gender">x-empty </tpl>x-value action-genderPicker-gender<tpl if="!gender"> x-empty</tpl>">',
												'<tpl if="!gender">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="gender">{gender}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//email
                '<tpl if="email || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('email') + ':</td>',
										'<td>',
											'<span class="<tpl if="!email">x-empty </tpl>x-value action-edit-email<tpl if="!email"> x-empty</tpl>">',
												'<tpl if="!email">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="email">{email}<div class="verified<tpl if="verified_email"> true</tpl><tpl if="!verified_email"> false</tpl><tpl if="can_edit"> cursor</tpl>"></div></tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//website
                '<tpl if="website || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('website') + ':</td>',
										'<td>',
											'<span class="<tpl if="!website">x-empty </tpl>x-value action-edit-website<tpl if="!website"> x-empty</tpl>">',
												'<tpl if="!website">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="website">{website}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//phone
                '<tpl if="phone || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('phone') + ':</td>',
										'<td>',
											'<span class="<tpl if="!phone">x-empty </tpl>x-value action-edit-phone<tpl if="!phone"> x-empty</tpl>">',
												'<tpl if="!phone">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="phone">{phone}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
                 
                '<hr />',
                 
								//birthday
								'<tpl if="birthday || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('birthday') + ':</td>',
										'<td>',
											'<span class="<tpl if="!birthday">x-empty </tpl>x-value action-datePicker-birthday<tpl if="!birthday"> x-empty</tpl>">',
												'<tpl if="!birthday">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="birthday">{birthday_date}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//sexuality
                '<tpl if="sexuality || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('sexuality') + ':</td>',
										'<td>',
											'<span class="<tpl if="!sexuality">x-empty </tpl>x-value action-edit-sexuality<tpl if="!sexuality"> x-empty</tpl>">',
												'<tpl if="!sexuality">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="sexuality">{sexuality}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//relationship
                '<tpl if="relationship || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('relationship') + ':</td>',
										'<td>',
											'<span class="<tpl if="!relationship">x-empty </tpl>x-value action-edit-relationship<tpl if="!relationship"> x-empty</tpl>">',
												'<tpl if="!relationship">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="relationship">{relationship}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//heritage
								'<tpl if="heritage || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('heritage') + ':</td>',
										'<td>',
											'<span class="<tpl if="!heritage">x-empty </tpl>x-value action-edit-heritage<tpl if="!heritage"> x-empty</tpl>">',
												'<tpl if="!heritage">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="heritage">{heritage}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//religion
								'<tpl if="religion || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('religion') + ':</td>',
										'<td>',
											'<span class="<tpl if="!religion">x-empty </tpl>x-value action-edit-religion<tpl if="!religion"> x-empty</tpl>">',
												'<tpl if="!religion">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="religion">{religion}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//political_view
								'<tpl if="political_view || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('political_view') + ':</td>',
										'<td>',
											'<span class="<tpl if="!political_view">x-empty </tpl>x-value action-edit-political_view<tpl if="!political_view"> x-empty</tpl>">',
												'<tpl if="!political_view">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="political_view">{political_view}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
								//diet
								'<tpl if="diet || can_edit">',
									'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
										'<td>' + Ext.ux.Translator.translate('diet') + ':</td>',
										'<td>',
											'<span class="<tpl if="!diet">x-empty </tpl>x-value action-edit-diet<tpl if="!diet"> x-empty</tpl>">',
												'<tpl if="!diet">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="diet">{diet}</tpl>',
											'</span>',
										'</td>',
									'</tr></table>',
								'</tpl>',
								
               '</div>',
             '</div>',
           '</div>',
         '</div>',
        
        '<tpl if="skype || linked_in || twitter || can_edit">',
        '<div class="x-section">',
 					// skype
 					'<tpl if="skype || can_edit">',
 						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
 							'<td>Skype:</td>',
 							'<td>',
 								'<span class="<tpl if="!skype">x-empty </tpl>x-value action-edit-skype<tpl if="!skype"> x-empty</tpl>">',
 									'<tpl if="!skype">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="skype">{skype}</tpl>',
 								'</span>',
 							'</td>',
 						'</tr></table>',
 					'</tpl>',
 					///////////
 					
 					// linked_in
 					'<tpl if="linked_in || can_edit">',
 						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
 							'<td>Linked In:</td>',
 							'<td>',
 								'<span class="<tpl if="!linked_in">x-empty </tpl>x-value action-edit-linked_in<tpl if="!linked_in"> x-empty</tpl>">',
 									'<tpl if="!linked_in">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="linked_in">{linked_in}</tpl>',
 								'</span>',
 							'</td>',
 						'</tr></table>',
 					'</tpl>',
 					///////////
 					
 					// twitter
 					'<tpl if="twitter || can_edit">',
 						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
 							'<td>Twitter:</td>',
 							'<td>',
 								'<span class="<tpl if="!twitter">x-empty </tpl>x-value action-edit-twitter<tpl if="!twitter"> x-empty</tpl>">',
 									'<tpl if="!twitter">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="twitter">{twitter}</tpl>',
 								'</span>',
 							'</td>',
 						'</tr></table>',
 					'</tpl>',
 					///////////
 					
 					// im
 					'<tpl if="im || can_edit">',
 						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
 							'<td>' + Ext.ux.Translator.translate('im') + ':</td>',
 							'<td>',
 								'<span class="<tpl if="!im">x-empty </tpl>x-value action-edit-im<tpl if="!im"> x-empty</tpl>">',
 									'<tpl if="!im">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="im">{im}</tpl>',
 								'</span>',
 							'</td>',
 						'</tr></table>',
 					'</tpl>',
 					///////////
 					
 					// blog
 					'<tpl if="blog || can_edit">',
 						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
 							'<td>' + Ext.ux.Translator.translate('app_blog') + ':</td>',
 							'<td>',
 								'<span class="<tpl if="!blog">x-empty </tpl>x-value action-edit-blog<tpl if="!blog"> x-empty</tpl>">',
 									'<tpl if="!blog">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="blog">{blog}</tpl>',
 								'</span>',
 							'</td>',
 						'</tr></table>',
 					'</tpl>',
 					///////////
 					
 					// other
 					'<tpl if="other || can_edit">',
 						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
 							'<td>' + Ext.ux.Translator.translate('other') + ':</td>',
 							'<td>',
 								'<span class="<tpl if="!other">x-empty </tpl>x-value action-edit-other<tpl if="!other"> x-empty</tpl>">',
 									'<tpl if="!other">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="other">{other}</tpl>',
 								'</span>',
 							'</td>',
 						'</tr></table>',
 					'</tpl>',
 					///////////
 				'</div>',
 				'<br />',
 				'</tpl>',
        
				'<div class="x-section">',
          '<h1>' + Ext.ux.Translator.translate('favorites') + '</h1>',
					
					// topics
					'<tpl if="topics || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('topics') + ':</td>',
							'<td>',
								'<span class="<tpl if="!topics">x-empty </tpl>x-value action-edit-topics<tpl if="!topics"> x-empty</tpl>">',
									'<tpl if="!topics">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="topics">{topics}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// foods
					'<tpl if="foods || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('foods') + ':</td>',
							'<td>',
								'<span class="<tpl if="!foods">x-empty </tpl>x-value action-edit-foods<tpl if="!foods"> x-empty</tpl>">',
									'<tpl if="!foods">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="foods">{foods}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// drinks
					'<tpl if="drinks || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('drinks') + ':</td>',
							'<td>',
								'<span class="<tpl if="!drinks">x-empty </tpl>x-value action-edit-drinks<tpl if="!drinks"> x-empty</tpl>">',
									'<tpl if="!drinks">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="drinks">{drinks}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// movies
					'<tpl if="movies || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('movies') + ':</td>',
							'<td>',
								'<span class="<tpl if="!movies">x-empty </tpl>x-value action-edit-movies<tpl if="!movies"> x-empty</tpl>">',
									'<tpl if="!movies">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="movies">{movies}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// tv_shows
					'<tpl if="tv_shows || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('tv_shows') + ':</td>',
							'<td>',
								'<span class="<tpl if="!tv_shows">x-empty </tpl>x-value action-edit-tv_shows<tpl if="!tv_shows"> x-empty</tpl>">',
									'<tpl if="!tv_shows">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="tv_shows">{tv_shows}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// music
					'<tpl if="music || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('music') + ':</td>',
							'<td>',
								'<span class="<tpl if="!music">x-empty </tpl>x-value action-edit-music<tpl if="!music"> x-empty</tpl>">',
									'<tpl if="!music">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="music">{music}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// books
					'<tpl if="books || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('books') + ':</td>',
							'<td>',
								'<span class="<tpl if="!books">x-empty </tpl>x-value action-edit-books<tpl if="!books"> x-empty</tpl>">',
									'<tpl if="!books">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="books">{books}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// role_models
					'<tpl if="role_models || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('role_models') + ':</td>',
							'<td>',
								'<span class="<tpl if="!role_models">x-empty </tpl>x-value action-edit-role_models<tpl if="!role_models"> x-empty</tpl>">',
									'<tpl if="!role_models">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="role_models">{role_models}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// places
					'<tpl if="places || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('places') + ':</td>',
							'<td>',
								'<span class="<tpl if="!places">x-empty </tpl>x-value action-edit-places<tpl if="!places"> x-empty</tpl>">',
									'<tpl if="!places">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="places">{places}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
					// quotes
					'<tpl if="quotes || can_edit">',
						'<table cellpadding="0"<tpl if="can_edit"> class="can_edit"</tpl>><tr>',
							'<td>' + Ext.ux.Translator.translate('quotes') + ':</td>',
							'<td>',
								'<span class="<tpl if="!quotes">x-empty </tpl>x-value action-edit-quotes<tpl if="!quotes"> x-empty</tpl>">',
									'<tpl if="!quotes">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="quotes">{quotes}</tpl>',
								'</span>',
							'</td>',
						'</tr></table>',
					'</tpl>',
					///////////
					
        '</div>',
				
				'<br />',
				
        '<tpl if="bio || can_edit">',
          '<div class="x-section">',
            '<h1>' + Ext.ux.Translator.translate('bio') + '</h1>',
            '<div class="action-edit-bio<tpl if="!bio"> x-empty</tpl>">',
							'<tpl if="!bio">' + Ext.ux.Translator.translate('app_add') + '</tpl><tpl if="bio">{bio_html}</tpl>',
            '</div>',
          '</div>',
        '</tpl>',
      '</div>',
    '</tpl>'
  ),

  //we have to override these to keep superclass happy :/
  getDataViewConfig: Ext.emptyFn,
  getDefaultHBar:    Ext.emptyFn
});

Ext.reg('kohive_app_idcards_view_show', Kohive.views.idcards.Show);

/**
 * @class Kohive.views.invite.Index
 * @extends Kohive.Window
 * The main window for inviting users
 */
Kohive.views.invite.Index = Ext.extend(Kohive.Window, {
  title:           Ext.ux.Translator.translate('invite_users'),
  cls:             'x-invite-index',
  url:             'invite',
  layout:          'border',
  width:           400,
  height:          350,
	draggable:       false,
	// resizable:       false,
	maximizable:     false,
	hasDefaultTools: false,
 
  /**
   * Creates the sub-view elements
   */
  initComponent: function() {
		this.initialConfig.maximizable = false;
		
		this.dataview = new Ext.DataView({
      autoHeight:     true,
      autoScroll:     true,
			multiSelect:    true,
			simpleSelect:   true,
      overClass:      'x-over',
      itemSelector:   'div.contact',
      tpl:            new Ext.XTemplate(
        '<tpl for=".">',
          '<div class="contact">',
						'<div class="checker"></div>',
						'<div class="name">{email}</div>',
						'<div class="email">{name}</div>',
          '</div>',
        '</tpl>'
      )
    });
		
		var cancelBtn = new Kohive.SmallButton({
			text:    Ext.ux.Translator.translate('cancel'),
			color:   'red',
			scope:   this,
			handler: this.hide
		});
		
		var importBtn = new Kohive.SmallButton({
			text:    Ext.ux.Translator.translate('iimport'),
			scope:   this,
			handler: this.onImport
		});
		
		this.formPanel = new Ext.Panel({
			cls: 'x-invite-body',
			items: [
				this.dataview
			],
			buttons: [
				cancelBtn,
				importBtn
			]
		});
		
    Ext.apply(this, {
      hbar:   false,
      bbar:   true,
      id:     'invite_index_' + Kohive.OS.getCurrentHiveId(),
			layout: 'fit',
			
			// This bad boy fixes horizontal scrolling issues in Safari only.
			autoScroll: false,
			
      items: [this.formPanel]
    });
    
    Kohive.views.invite.Index.superclass.initComponent.apply(this, arguments);
		
    this.initListeners();
  },
  
  /**
   * Listen to events fired by the controller and sub-views
   */
  initListeners: function() {
    var controller = Kohive.OS.getController('invite');
		
		controller.on({
			scope               : this,
			'import-successful' : this.hide,
			'import-failed'     : this.showFailure
		});
			
		this.on({
			scope    : this,
			'show'   : this.onShow,
			
			'contacts-loaded' : this.showSuccess
		});
  },

	
	initEvents: function() {
		Kohive.views.invite.Index.superclass.initEvents.apply(this, arguments);
		
		this.addEvents(
		  /**
		   * @event contacts-loaded
		   * Fires when the contacts have been loaded
		   * @param {Ext.data.SimpleStore} store The store of contacts
		   */
		  'contacts-loaded',
		
			/**
			 * @event import-contacts
			 * Fires when the user wants to import contacts
			 * @param {Ext.Record} records An array of records/contacts
			 */
			'import-contacts'
		);
	},
	
	onShow: function() {
		this.dataview.hide();
		this.getContacts();
		this.center();
	},
	
	getContacts: function() {
		if (!this.contact) return;
		
		this.showLoading(Ext.ux.Translator.translate('app_statusbar_loading'));
		
		var req = new Kohive.models.Contact({
			service:  this.contact.service,
			login:    this.contact.login,
			password: this.contact.password
		});
		
		req.save({
			url:   '/memberships/' + Kohive.OS.getCurrentHiveId() + '/contacts',
			scope: this,
			success: function(contact, response) {
				var response = Ext.decode(response.responseText);
				if (response.contacts) this.importContacts(response.contacts);
			},
			failure: function() {
				this.showFailure();
				Ext.Msg.alert('', Ext.ux.Translator.translate('import_authentication_error'), function() {
					//hide the invite window
					this.hide();
					
					//show the contacts window again
					var controller = Kohive.OS.getController('members');
					controller.fireAction('new');
					controller.getRunningView('addMemberPanel').switchToContacts(this.contact);
				}, this);
			},
			callback: function() {
				this.hideLoading();
			}
		});
	},
	
	/**
	 * Converts the contacts into a store and adds it to the dataview
	 * @param {Array} array An array of contacts to show to the user
	 */
	importContacts: function(array) {
		var store = new Ext.data.SimpleStore({
			data: array,
			fields: ['name', 'email']
		});
		
		this.dataview.setStore(store);
		this.dataview.show();
		
		this.fireEvent('contacts-loaded', store);
	},
	
	selectAll: function() {
		//select all nodes
		var nodes = this.dataview.getNodes() || [];
		Ext.each(nodes, function(node) {
		  this.dataview.select(node);
		}, this);
	},
	
	/**
	 * 
	 */
	onImport: function() {
		//get all the selected records
		var records = this.dataview.getSelectedRecords();
		
		this.showLoading(Ext.ux.Translator.translate('app_statusbar_loading'));
		
		this.fireEvent('import-contacts', records);
	}
});

Ext.reg('kohive_app_invite_view_index', Kohive.views.invite.Index);

/**
 * @class Kohive.views.photos.Viewer
 * @extends Ext.Panel
 * Displays an album selector and the images for the selected album
 * @cfg {Ext.data.Store} albumStore The Ext.data.Store subclass to use for the albums combo box (required)
 */
Kohive.views.photos.Viewer = Ext.extend(Ext.Panel, {
	/**
	 * @property deletingAlbum
	 * @type Boolean
	 * Tru when the user is deleting an album
	 */
  deletingAlbum: false,
	
	
  initComponent: function() {
    /**
     * @property controller
     * @type ExtMVC.Controller
     * A reference to the controller controlling this view
     */
    this.controller = Kohive.OS.getController('photos');
    
    /**
     * @property hiveId
     * @type Number
     * A reference to the Hive ID this viewer is for.  This is set to the ID of the current hive when this component
     * is created.  It is required because we only want to listen to events from the photos controller when they are
     * fired for this particular hive.  Each hive has its own instance of the photos application so we only listen
     * to events from the relevant hive.
     */
    this.hiveId = Kohive.OS.getCurrentHive().get('hive_id');
    
    /**
     * @property albumsCombo
     * @type Ext.form.ComboBox
     * The album selector combo box
     */
    this.albumsCombo = new Ext.form.ComboBox({
      cls:            'photos-combobox',
      mode:           'local',
      store:          this.albumStore,
      editable:       false,
      triggerAction:  'all',
      displayField:   'title',
      valueField:     'id',
      emptyText:      Ext.ux.Translator.translate('app_statusbar_loading'),
      selectOnFocus:  true,
      width:          160,
      maxHeight:      200,
			tpl:            new Ext.XTemplate(
				'<tpl for=".">',
					'<div class="x-combo-list-item">',
						'<tpl if="xcount &gt; 1"><tpl if="can_delete"><div class="x-delete"></div></tpl></tpl>',
						'{title}',
					'</div>',
				'</tpl>'
			)
    });
    
    /**
     * @property albumField
     * @type Ext.form.TextField
     * Description
     */
    this.albumField = new Ext.form.TextField({
      cls:             'app-album-field',
      name:            'title',
      width:           160,
      height:          19,
      enableKeyEvents: true,
      hidden:          true,
      allowBlank:      false,
      validationEvent: false
    });
    
    /**
     * @property addBtn
     * @type Kohive.Button
     * Description
     */
    this.addBtn = new Kohive.IconButton({
      cls:     'photos-button add',
      iconCls: 'new-album',
      width:   11,
      scope:   this
    });
    

    /**
     * @property albumManager
     * @type Ext.Panel
     * A panel which has a combobox to display all the albums, also has buttons
     * to add and destroy albums.
     */
    this.albumManager = new Ext.Panel({
      cls:    'x-app-album-manager',
      region: 'north',
			border: false,
      height: 19,
      items:  [
        this.albumsCombo,
        this.albumField,
        this.addBtn
      ]
    });
    
    /**
     * @property photosDataView
     * @type Ext.DataView
     * DataView used to render the photos for the current album
     */
    this.photosDataView = new Ext.DataView({
			cls:            'x-app-viewer',
      emptyText:      '<div class="x-image-blank"></div>',
      deferEmptyText: false,
      singleSelect:   true,
      autoHeight:     true,
      autoScroll:     true,
      overClass:      'x-over',
      itemSelector:   'div.thumbnail',
      tpl:            new Ext.XTemplate(
        '<tpl for=".">',
          '<div class="thumbnail-wrapper">',
            '<div class="thumbnail">',
  						'<tpl if="can_delete"><div class="x-delete"></div></tpl>',
              '<img src="{thumb_filename}" />',
            '</div>',
          '</div>',
        '</tpl>'
      )
    });
    
    /**
     * @property photoScroller
     * @type Ext.Panel
     * A scroller which displays all photos in the current album
     */
    this.photoScroller = new Ext.Panel({
      cls:        'x-photos-scroller',
      region:     'center',
			border:     false,
      autoScroll: true,
      items:      [this.photosDataView]
    });
    
    Ext.applyIf(this, {
			border: false,
			layout: 'border',
      items:  [
        this.albumManager,
        this.photoScroller
      ]
    });
    
    Kohive.views.photos.Viewer.superclass.initComponent.apply(this, arguments);
    
    this.initEvents();
    this.initListeners();
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event album-selected
       * Fires when an album is selected in the combo box
       * @param {Kohive.models.Album} album The selected Album
       */
      'album-selected',
      
      /**
       * @event create-album
       * Fires when the user has entered and album name and clicked the 'Add' button or pressed enter
       * @param {String} albumName The name of the album to create
       */
      'create-album',
      
      /**
       * @event create-rss-album
       * Fires when the user has entered the feed url and clicked the 'Add' button or pressed enter
       * @param {String} feedUrl The url of the feed to create
       */
      'create-rss-album',
      
      /**
       * @event photo-clicked
       * Fired when a photo in the photos DataView is clicked
       * @param {Kohive.models.Image} photo The photo that was clicked
       */
      'photo-clicked',
			
			/**
			 * @event destroy-image
			 * Fires when the user wants to destroy a photo
			 * @param {Kohive.models.Image} photo The photo that was clicked
			 */
			'destroy-image',
			
			/**
			 * @event load-album
			 * Fires when the album needs to be loaded
			 * @param {Kohive.models.Album} album The album that needs to be loaded
			 */
			'load-album'
    );    
  },

  /**
   * Attaches listeners to the controller child elements
   */
  initListeners: function() {
    this.controller.on({
      scope:                this,
      'images-loaded':      this.reloadImages,
      'update':             this.updateStore,
      'album-created':      this.onAlbumCreated,
      'rss-album-created':  this.onRssAlbumCreated,
      'album-destroyed':    this.onAlbumDestroyed
    });
    
    this.albumsCombo.on(   'select',       this.onComboSelect, this);
    this.albumsCombo.on(   'click',        this.onComboClick, this);
    this.albumsCombo.on(   'beforeselect', this.canSelectAlbum, this);
    this.addBtn.on(        'click',        this.onAddButtonClick, this);
    this.albumField.on(    'keyup',        this.onSpecialKey, this);
    this.photosDataView.on('click',        this.onPhotoClick, this);
    
    var firstTime = true;
    this.albumStore.on('load', function(store, records, options) {
      if (firstTime) {
				// Select the first album in the combo + fire off the select event as it isn't
				// fired when using setValue
				this.albumsCombo.setValue(records[0].get('id'));
				this.albumsCombo.fireEvent('select', this, records[0], 0);
				
				// Enable the upload and refresh buttons
				// Ext.getCmp('upload_photo_button').enable(); we dont have to enable this, as it gets overlayed with flash
				Ext.getCmp('refresh_photo_button' + Kohive.OS.getCurrentHiveId()).enable();
				
				firstTime = false;
				
				return;
			};
    }, this);
  },
  
	updateStore: function(image) {
		var store = this.photosDataView.getStore();
		if (!store) return;
		
		var recId = null;
		
		store.each(function(record, id) {
			if (record.get('id') == image.get('id')) recId = id;
		}, this);
		
		//update the record
		this.photosDataView.store.data.items[recId] = image;
	},
	
  /**
   * Called when the Add button is clicked.  Hides and shows albums combo and new album field
   * as appropriate
   */
  onAddButtonClick: function() {
    if (this.enteringNewAlbum()) {
      this.onCreateAlbum();
    } else {
      this.toggleNewAlbumForm();
    }
  },
  
  /**
   * Toggles between showing the existing albums combo and the new album form
   */
  toggleNewAlbumForm: function() {
    var button = this.addBtn.el.child('button');
        
    //toggle visibility of the textfield and the combo box
    Ext.each([this.albumField, this.albumsCombo], function(f) {f.setVisible(f.hidden);});
    button.toggleClass('new-album').toggleClass('submit-album');
    
    //select textfield if adding
    if (this.albumField.isVisible()) this.albumField.focus();
  },
  
  /**
   * Returns true if the user is currently entering a new album name
   * @return {Boolean} True if the user is currently entering a new album name
   */
  enteringNewAlbum: function() {
    return this.albumsCombo.hidden;
  },
  
  /**
   * Called when the Add button is clicked while in new album mode, or when the enter key is pressed
   * inside the album field.
   */
  onCreateAlbum: function() {
		// Check if the value is empty or not
		if (!this.albumField.isValid()) return this.toggleNewAlbumForm();
		
    this.fireEvent('create-album', this.albumField.getValue());
  },
  
  /**
   * Called when the Add button is clicked while in new rss album mode, or when the enter key is pressed
   * inside the album field.
   */
  onCreateRssAlbum: function() {
		// Check if the value is empty or not
    if (!this.rssAlbumField.isValid()) return this.toggleNewRssAlbumForm();
    
    this.fireEvent('create-rss-album', this.rssAlbumField.getValue());
  },
  
  /**
   * Called when the user chooses a new item in the albums combo box
   */
  onComboSelect: function(combo, record, index) {
    this.fireEvent('album-selected', record);
  },

	/**
   * Called when the user clicks anywhere in the combo box.
   */
  onComboClick: function(dataview, index, node, e) {
		// Get the target element
		var el = Ext.get(e.getTarget());
		
		// Get the album
		var album = dataview.getRecord(node);
		
		// Check if it has the delete class
		if (el.hasClass('x-delete')) this.onAlbumDelete(album);
  },
  
  /**
   * Called when ENTER or ESC keys are pressed while new album field is focused
   * @param {Ext.form.Field} field The field firing the event
   * @param {Event} e The keypress event
   */
  onSpecialKey: function(field, e) {
    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) this.onCreateAlbum();    
    if (e.getKey() == e.ESC) this.toggleNewAlbumForm();
  },
  
  onRssAlbumFieldSpecialKey: function(field, e) {
    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) this.onCreateRssAlbum();
    if (e.getKey() == e.ESC) this.toggleNewRssAlbumForm();
  },
  
  /**
   * Called when a photo in the dataview is clicked.  Emits a photo-clicked event
   * @param {Ext.DataView} dataview The photos dataview
   * @param {Number} index The index of the clicked photo
   * @param {HTMLElement} node The node that was clicked on
   */
  onPhotoClick: function(dataview, index, node, e) {
		// Get the target element the user clicked on
		var el = Ext.get(e.getTarget());
		
		// Get the photo record using the node the user clicked on (always the thumbnail)
    var photo = dataview.getRecord(node);
		
		// check if it has the x-delete class
		if (el.hasClass('x-delete')) return this.onDelete(photo);
		
		// It doesn't have the delete class, so just fire the photo clicked event 
		this.fireEvent('photo-clicked', photo);
  },
	
	/**
	 * Returns false if the user is deleting an album
	 */
	canSelectAlbum: function() {
		if (this.deletingAlbum) return false;
		
		return true;
	},
  
  /**
   * Called when the controller indicates a new album has been successfully created
   * Refreshes the combo store and calls toggleNewAlbumForm
   * @param {Kohive.models.Album} album The newly created album
   */
  onAlbumCreated: function(album) {
		this.ifCurrentHive(function() {
			this.albumField.reset();
			this.toggleNewAlbumForm();
			
			this.albumsCombo.store.on('load', this.showLastAlbum, this);
			this.albumsCombo.store.reload();
		});
  },
  
  /**
   * Called when the controller indicates a new rss album has been successfully created
   * Refreshes the combo store and calls toggleNewRssAlbumForm
   * @param {Kohive.models.Album} album The newly created album
   */
  onRssAlbumCreated: function(album) {
		this.ifCurrentHive(function() {
			this.rssAlbumField.reset();
			this.toggleNewRssAlbumForm();
			
			this.albumsCombo.store.on('load', this.showLastAlbum, this);
			this.albumsCombo.store.reload();
		});
  },


	/**
	 * Called when the controller says an album has been destroyed
   * @param {Kohive.models.Album} album The newly destroyed album
	 */
	onAlbumDestroyed: function(album) {
		var currentAlbum = this.controller.currentAlbum[Kohive.OS.getCurrentHiveId()];
		
		if (currentAlbum.get('id') == album.get('id')) this.showFirstAlbum();
		
		this.albumsCombo.store.reload();
	},
	
	/**
	 * Shows the last album from the alabum combobox
	 */
	showLastAlbum: function(store) {
		var store = store || this.albumsCombo.store;
		
		this.albumsCombo.store.un('load', this.showLastAlbum, this);
		
		var album = store.getAt(store.data.items.length - 1);
		if (album) this.fireEvent('load-album', album);
	},
	
	/**
	 * Shows the first album from the combobox
	 */
	showFirstAlbum: function() {
		var store = this.albumsCombo.store;
		
		var album = store.getAt(0);
		if (album) this.fireEvent('load-album', album);
	},
	
	/**
	 * Called when an album is delete from the combobox
	 * @param {Kohive.models.Album} album Instance of the album which needs to be deleted
	 */
	onAlbumDelete: function(album) {
		this.deletingAlbum = true;
		
		return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('destroy_album_desc', { title: album.get('title')}),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
          this.fireEvent('destroy-album', album);
        };
				
				this.deletingAlbum = false;
      }   
    });
	},
	
	/**
	 * 
	 * @param {Kohive.models.Photo} photo Instance of the photo which needs to be deleted
	 */
	onDelete: function(photo) {
		return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('destroy_image_desc', { title: photo.get('title')}),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
          this.fireEvent('destroy-image', photo);
        }
      }   
    });
	},
  
  /**
   * Updates the Images list with a new store
   * @param {Ext.data.Store} store The store containing images to display
   * @param {Kohive.models.Album} album The Album whose images have been loaded 
   */
  reloadImages: function(store, album) {
    this.ifCurrentHive(function() {
      this.photosDataView.setStore(store);
      this.albumsCombo.setValue(album.get('id'));      
    });
  },
  
  /**
   * Runs a function if the current hive's ID matches this.hiveId
   * @param {Function} fcn A function to run if the current hive matches
   * @param {Object} scope The scope to execute the function in (defaults to the Viewer scope)
   */
  ifCurrentHive: function(fcn, scope) {
    var currentHiveId = Kohive.OS.getCurrentHive().get('hive_id');
    
    if (currentHiveId == this.hiveId) fcn.call(scope || this);
  }
});

Ext.reg('photos_viewer', Kohive.views.photos.Viewer);

/**
 * @class Kohive.views.photos.Index
 * @extends Kohive.Window
 * The main photos application window - houses albums panel and an image panel
 */
Kohive.views.photos.Index = Ext.extend(Kohive.Window, {
  title:    Ext.ux.Translator.translate('app_photos'),
  cls:      'x-photos-index',
  url:      'photos',
  layout:   'border',
  width:    800,
	minWidth: 800,
  height:   490,
  defaults: {border: false},
 
  /**
   * Creates the sub-view elements
   */
  initComponent: function() {
    /**
     * @property imageView
     * @type Ext.Panel
     * Reference to the image view
     */
    this.imageView = new Kohive.views.photos.Image({
      hidden: true,
      id:     'photos-viewer' + Kohive.OS.getCurrentHiveId(),
      listeners: {
        scope:  this,
        'delete': function(image) {
          this.showDestroying();
          this.fireEvent('destroy-image', image);
        },
        'update': function(image, fields, forceRefresh) {
          this.showUpdating();
          this.fireEvent('update-image', image, fields, forceRefresh);
        }
      }
    });
		
		/**
		 * @property newPhotoFromUrlView
		 * @type Kohive.views.photos.New
		 * Reference to the new photo from Url panel
		 */
		this.newPhotoFromUrlView = new Kohive.views.photos.New();
		
		/**
		 * @property newPhotoFromFlickrView
		 * @type Kohive.views.photos.New
		 * Reference to the new photo from Url panel
		 */
		this.newPhotoFromFlickrView = new Kohive.views.photos.NewFlickr();
    
    /**
     * @property commentsNew
     * @type Ext.Panel
     * Reference to the new comment view
     */
    this.commentsNew = new Kohive.views.comments.New({
      hidden: true,
      id:     'comments-new-photos' + Kohive.OS.getCurrentHiveId()
    });

    /**
     * @property commentsIndex
     * @type Ext.Panel
     * Reference to the comments view
     */
    this.commentsIndex = new Kohive.views.comments.Index({
      hidden: true,
      id:     'comments_index_photos' + Kohive.OS.getCurrentHiveId()
    });
    
    /**
     * @property photoViewer
     * @type Kohive.views.photos.Viewer
     * Reference to the photo viewer panel
     */
    this.photoViewer = new Kohive.views.photos.Viewer({
      cls:        'x-menu',
      albumStore: this.albumStore,
      listeners:  {
        scope: this,
        'album-selected': function(album) {
          this.fireEvent('load-album', album);
        },
        'photo-clicked': function(photo) {
          this.fireEvent('show-image', photo);
        }
      }
    });
		
    this.relayEvents(this.photoViewer, ['create-album', 'create-rss-album', 'destroy-image', 'destroy-album', 'load-album']);
    
		var controller = Kohive.OS.getController('photos');
		
    Ext.apply(this, {
      plugins:    [new Ext.ux.HeaderToolbar()],
      hbar: [
        {
          text:     Ext.ux.Translator.translate('upload'),
          id:       'upload_photo_button' + Kohive.OS.getCurrentHiveId(),
          cls:      'x-upload-button',
          iconCls:  'upload',
          xtype:    'header_toolbar_button',
					disabled: true,
          scope:    this
        },
				{
          text:     Ext.ux.Translator.translate('url'),
          id:       'upload_photo_from_file_button' + Kohive.OS.getCurrentHiveId(),
          iconCls:  'newurl',
          xtype:    'header_toolbar_button',
          scope:    this,
					handler: function() {
						var other = this.newPhotoFromFlickrView;
						other.collapse();
						
						var f = this.newPhotoFromUrlView;
						f.toggleCollapse();
			      f.doLayout();

						// Get the first item and focus on it
						var field = f.find()[0];
						if (field) {
							var tempTask = new Ext.util.DelayedTask(function() {
							  field.focus();
							}, this);
							tempTask.delay(500);
						};
					}
        },
        new Ext.ux.HeaderToolbarButton({
	        id:      'flickr_photo_button' + Kohive.OS.getCurrentHiveId(),
					text:    Ext.ux.Translator.translate('add_from_flickr'),
          iconCls: 'flickr',
					scope:    this,
					handler: function() {
						var other = this.newPhotoFromUrlView;
						other.collapse();
						
						var f = this.newPhotoFromFlickrView;
						f.toggleCollapse();
			      f.doLayout();

						// Get the first item and focus on it
						var field = f.find()[0];
						if (field) {
							var tempTask = new Ext.util.DelayedTask(function() {
							  field.focus();
							}, this);
							tempTask.delay(500);
						};
					}
        }),
				{
          text:     Ext.ux.Translator.translate('refresh'),
          id:       'refresh_photo_button' + Kohive.OS.getCurrentHiveId(),
          iconCls:  'refresh',
          xtype:    'header_toolbar_button',
					disabled: true,
          handler: function() { 
            this.fireEvent('refresh');
          },
          scope:   this
        }
      ],
      bbar:     true,
      filterPanelConfig: {
        filters: [
          {
            ftype:        'store-helper',
            displayField: 'tags.name',
            valueField:   'tags.name',
            title:        Ext.ux.Translator.translate('tags'),
            iconCls:      'tag',
            view:         controller,
            listenTo: {
              component: controller,
              events:    ['images-loaded']
            }
          },
          {
            ftype:        'store-helper',
            displayField: 'author_nickname',
            valueField:   'author_membership_id',
            title:        Ext.ux.Translator.translate('authors'),
            iconCls:      'author',
            view:         controller,
            listenTo: {
              component: controller,
              events:    ['images-loaded']
            }
          }
        ],
        listeners: {
          scope            : this,
          'filters-updated': this.onFiltersUpdated
        }
      },
      id:    'photos_index_' + Kohive.OS.getCurrentHiveId(),
      items: [
        {
          cls:         'x-menu-wrap',
          region:      'west',
          width:       180,
          collapsible: false,
          layout:      'fit',
          items:       [this.photoViewer]
        },
        {
          region:     'center',
          cls:        'x-content-wrap',
          autoScroll: true,
          items: [
						this.newPhotoFromUrlView,
						this.newPhotoFromFlickrView,
            this.imageView,
            this.commentsIndex,
            this.commentsNew
          ]
        }
      ]
    });
    
    Kohive.views.photos.Index.superclass.initComponent.apply(this, arguments);
    
    this.initListeners();
  },
  
  /**
   * Called when the images-loaded event is fired by the controller (e.g. new album loaded).
   * This sets up the flash upload for this album
   * @param {Ext.data.Store} images The images to load
   * @param {Kohive.models.Album} album The album that has been loaded
   */
  onImagesLoaded: function(images, album) {
		this.hideLoading();
		
		//init the tag store
		this.initTagsStore(images);
		
    this.getSessionInfo(function(session) {
      //TODO: Getting the url like this relies on a hack used by the controller.  Again, this should be in model
      var url   = Kohive.OS.getController('photos').scopedUrl('albums/' + album.get('id') + '/images.json'),
          key   = session.get('_kohive_session'),
          token = session.get('auth_token'),
          win   = this; //needed for flash callbacks below
          
      // Check if the swf is already there, if it is, remove it
      var currentUploader = this.el.child('.fileUploaderBtn');
      if (currentUploader) currentUploader.remove();
      var currentUploader = this.el.child('.fileUploadQueue');
      if (currentUploader) currentUploader.remove();
      var currentUploader = Ext.get('upload_photo_button' + Kohive.OS.getCurrentHiveId() + 'Uploader');
      if (currentUploader) currentUploader.remove();
      
      $('#upload_photo_button' + Kohive.OS.getCurrentHiveId()).fileUpload({
        'uploader':       '/flash/uploader.swf',
        'script':         url,
        'scriptData':     { '_kohive_session': key, 'auth_token': token },
        'fileDataName':   'image[file]', // Extract correct name of upload field from form field
        //'scriptAccess': 'always', // Incomment this, if for some reason it doesn't work
        'multi':          true,
        'auto':           true,
        'width':          38,
        'height':         45,
        'wmode':          'transparent',
        'fileDesc':       'png, jpg',
        'fileExt':        '*.jpg;*.png;*.jpeg;*.gif;',
        'sizeLimit':      5242880,
        'simUploadLimit': 150,
        'onSelect':       function() {
          win.newPhotoFromUrlView.textField.setValue('');
          win.newPhotoFromFlickrView.textField.setValue('');
          win.newPhotoFromUrlView.collapse();
          win.newPhotoFromFlickrView.collapse();
        },
				'onProgress':     function(e, queueId, fileObj, data) {
					win.addToProgresser(queueId, fileObj, data);
				},
        'onStart':        function() {
          win.showLoading(Ext.ux.Translator.translate('app_statusbar_uploading'));
          win.fireEvent('upload-started');
					win.setupProgresser();
        },
        'onAllComplete':  function(event, data) {
          win.fireEvent('upload-complete');
          win.fireEvent('refresh');
					
					win.pb.fadeOut.defer(1500, win.pb);
					
					var tempTask = new Ext.util.DelayedTask(function() {
					  win.showSuccess();
					}, this);
					tempTask.delay(300);
        }
      });
    });
  },
  
	files:     {},
	
	/**
	 * Sets up a progresser on the window. Only for testing purposes.
	 * place holder for the actual progress bar
	 */
	setupProgresser: function() {
		//update the files and the number of files being uploaded
		this.files     = {};
		
		//create the progress bar
		if (!this.pb) {
			var pbContainer = this.el.child('.x-statusbar');
			this.pb = pbContainer.createChild({
				cls: 'x-progress-bar',
				children: [
					{
						tag: 'div'
					}
				]
			});
		
			this.progressBar = this.pb.child('div');
			this.pb.hide();
		};
		
		this.pb.show();
		this.progressBar.setWidth(24);
		this.progressBar.update("1%");
	},
	
	/**
	 * Adds a file to the progresser, and if it is already there it updates its progress percentage
	 * @param {String} id The id of the file
	 * @param {Object} fileObj Object with details of the file
	 * @param {Object} data Data of the file, including percentage
	 */
	addToProgresser: function(id, fileObj, data) {
		//add/update the file to the loggggg
		this.files[id] = [fileObj, data.percentage];
		
		//update the progress bar
		this.updateProgressBar();
	},
	
	/**
	 * Updates the progress bar.
	 * - Calculates the total amount of files. 
	 */
	updateProgressBar: function() {
		var totalFiles      = 0,
				totalPercentage = 0;
		
		//loop through each of the files, get the total amount of files and the total percentage
		for (var file in this.files) {
			totalFiles++;
			totalPercentage = totalPercentage + this.files[file][1];
		};
		
		var actualPercentage = Math.round(totalPercentage / totalFiles);
		this.progressBar.setWidth(actualPercentage.toString() + "%");
		this.progressBar.update(actualPercentage.toString() + "%");
		
		if (actualPercentage == 100) {
			this.showLoading(Ext.ux.Translator.translate('app_statusbar_processing'));
			this.pb.fadeOut.defer(1000, this.pb);
		};
	},
	
  /**
   * Called when the controller fires its show-image event. This tells the comments form and index
   * to show themselves
   * @param {Kohive.models.Image} image The image to show
   */
  onShowImage: function(image, newImage) {
		this.commentsIndex.hide();
		this.commentsNew.hide();
		
    this.commentsIndex.setReferenceObject(image);
    this.commentsNew.setReferenceObject(image);
		
    if (image) {
		  var tempTask = new Ext.util.DelayedTask(function() {
			  this.commentsNew.show();
			  this.commentsNew.el.hide();
			  this.commentsNew.el.fadeIn({duration:0.3});
			}, this);
			tempTask.delay(300);
		};
  },
  
  /**
   * Called whenever the user changes the filters applied in the filter bar. Fires the 'filter' event
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  onFiltersUpdated: function(filtersApplied) {
    this.fireEvent('filter', filtersApplied);
  },
  
  /**
   * Called when a new album has been created by the controller
   * @param {Kohive.models.Album} album The newly created album
   */
  onAlbumCreated: function(album) {
		this.hideLoading();
  },
  
  /**
   * Called when a new rss album has been created by the controller
   * @param {Kohive.models.Album} album The newly created album
   */
  onRssAlbumCreated: function(album) {
		this.hideLoading();
  },
  
  /**
   * Listen to events fired by the controller and sub-views
   */
  initListeners: function() {
    var controller = Kohive.OS.getController('photos');
    
    controller.on({
      scope: this,
      'before-create-image'  : this.showCreating.createDelegate(this, [Ext.ux.Translator.translate('app_statusbar_uploading')]),
      'before-create-album'  : this.showCreating.createDelegate(this, [Ext.ux.Translator.translate('app_statusbar_uploading')]),
      'before-destroy-album' : this.showDestroying.createDelegate(this, []),
      'album-created'        : this.onAlbumCreated,
      'rss-album-created'    : this.onRssAlbumCreated,
      'album-destroyed'      : this.showSuccess,
      'update'               : this.showSuccess,
      'image-destroyed'      : this.showSuccess,
      'image-created'        : this.closeNewPhotoView,
      'create-image-failed'  : this.showSuccess.createDelegate(this, []),
      'before-load-images'   : this.showRefreshing,
      'images-loaded'        : function (imagesStore, album) {
        this.originalStore = Kohive.OS.getController('photos').originalStore;
        this.onImagesLoaded(imagesStore, album);
        this.onStoreLoad(imagesStore);
       },
       // 'images-loaded'       : this.onImagesLoaded,
      'show-image'           : this.onShowImage
    });
    
		this.on('show',          function() { this.fireEvent('refresh', this); }, this);
		this.on('destroy-image', this.showDestroying,                             this);
		
    controller.on('images-loaded', function(images, album) {
      this.fireEvent('update-filter-bar', album);
    }, this);
		controller.on('before-images-loaded', function(album) {
      this.fireEvent('update-filter-bar', album);
    }, this);
		controller.on('update', function() {
      this.fireEvent('update-filter-bar', controller.currentAlbum[Kohive.OS.getCurrentHiveId()]);
    }, this);
  },
  
  onStoreLoad: function (store) {
  },
	
	/**
	 * Shows success message on the window and hides the new photo view
	 */
	closeNewPhotoView: function() {
		this.showSuccess();
		this.newPhotoFromUrlView.collapse();
		this.newPhotoFromFlickrView.collapse();
		
		this.fireEvent('refresh', this);
	},
  
  /**
   * Sets up all events emitted by this component.
   * This is called by the superclass so no need to call it from initComponent or constructor
   */
  initEvents: function() {
    Kohive.views.photos.Index.superclass.initEvents.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event create-album
       * Fires when the user attempts to create a new album
       * This is relayed from the Viewer sub-view component
       */

      /**
       * @event create
       * Fires when the user attempts to upload (create) an image
       */
      'create',
      
      /**
       * @event refresh
       * Fires when the user clicks the refresh button
       */
      'refresh',
      
      /**
       * @event update
       * Fires when the user wishes to update an Image
       * @param {Kohive.models.Image} image The Image to update
       * @param {Object} fields The fields object to update with
       */
      'update-image',
      
      /**
       * @event load-album
       * Fires when the user attempts to load an album and its images
       */
      'load-album',
      
      /**
       * @event destroy-image
       * Fires when the user attempts to destroy an image
       */
      'destroy-image',
      
      /**
       * @event upload-started
       * Fires when a flash-powered upload begins
       */
      'upload-started',
      
      /**
       * @event upload-complete
       * Fires when a flash-powered upload is complete
       */
      'upload-complete',
      
      /**
       * @event update-filter-bar
       * Fires when the attached filter bar needs to update itself as a new album is being viewed
       * @param {ExtMVC.Model.Base} album The album that was just loaded
       */
      'update-filter-bar'
    );
  },
	
  /**
   * @property sessionInfo
   * @type Kohive.models.Session|Null
   * A cached copy of the Session model instance.  Don't access directly, use getSessionInfo instead
   */
  sessionInfo: null,
  
  /**
   * Calls back with a Session model object. If the session has not already been loaded it is
   * loaded first
   * @param {Function} callback The function to call when the session data has been loaded
   * This will be called with a single argument - the Kohive.model.Session model object
   * @param {Function} scope The scope to call the callback in (optional)
   */
  getSessionInfo: function(callback, scope) {
    scope = scope || this;
    
    if (this.sessionInfo == null) {
      Kohive.models.Session.find(1, {
        url:     '/session/get',
        scope:   this,
        success: function(session) {
          this.sessionInfo = session;
          callback.call(scope, this.sessionInfo);
        }
      });
    } else callback.call(scope, this.sessionInfo);
  }
});

Ext.reg('kohive_app_photos_view_index', Kohive.views.photos.Index);

/**
 * @class Kohive.views.photos.New
 * @extends Kohive.views.pod.New
 */
Kohive.views.photos.New = Ext.extend(Kohive.views.pod.New, {
	
  /**
   * Set up the component, create references to textarea and counter
   */
  constructor: function(config) {
    var config = config || {};
		
		this.textField = new Ext.form.TextField({
      name:       'url',
      fieldLabel: Ext.ux.Translator.translate('url'),
			allowBlank: false
		});
		
    Ext.applyIf(config, {
      defaults: {
        anchor: "-15"
      },
      items: [
        this.textField
      ],
			submitButtonConfig: {
				text: Ext.ux.Translator.translate('add')
			}
    });

    Kohive.views.photos.New.superclass.constructor.call(this, Kohive.models.Image, config);
		
		this.initListeners();
  },
	
	initListeners: function() {
		//setup the tooltip so it shows when the user rolls over the new field
		this.on('expand', function() {
			var controller = Kohive.OS.getController('photos');
			if (!controller || !controller.indexWindow) return;
			
			this.textField.el.on('mouseover', function() { controller.indexWindow.fireEvent('statusbar-event-start', Ext.ux.Translator.translate('app_photos_url_field_desc')); }, this);
			this.textField.el.on('mouseout',  function() { controller.indexWindow.fireEvent('statusbar-event-stop'); }, this);
		}, this);
		
		this.controller = Kohive.OS.getController('photos');
		this.controller.on({
		  scope           : this,
		  'image-created' : this.onSuccess
		});
	},
	
	onCreate: function() {
    var form       = this.getForm();
		
    // Check if the form is valid
    if (form.isValid()) {
			var value = form.getValues().url;
			
      this.controller.createFromUrl(value);
      this.doLayout();
    };
  },
  
  onSuccess: function() {
    this.textField.setValue('');
  }
});

/**
 * @class Kohive.views.photos.NewFlickr
 * @extends Kohive.views.pod.New
 */
Kohive.views.photos.NewFlickr = Ext.extend(Kohive.views.pod.New, {
	
  /**
   * Set up the component, create references to textarea and counter
   */
  constructor: function(config) {
    var config = config || {};
		
		this.textField = new Ext.form.TextField({
	    name:       'url',
      fieldLabel: Ext.ux.Translator.translate('flickr_url'),
			allowBlank: false
	  });
		
    Ext.applyIf(config, {
      defaults: {
        anchor: "-15"
      },
      items: [
        this.textField
      ],
			submitButtonConfig: {
				text: Ext.ux.Translator.translate('add')
			}
    });

    Kohive.views.photos.NewFlickr.superclass.constructor.call(this, Kohive.models.Image, config);
		
		this.initListeners();
  },
	
	initListeners: function() {
		//setup the tooltip so it shows when the user rolls over the new field
		this.on('expand', function() {
			var controller = Kohive.OS.getController('photos');
			if (!controller || !controller.indexWindow) return;
			
			this.textField.el.on('mouseover', function() { controller.indexWindow.fireEvent('statusbar-event-start', Ext.ux.Translator.translate('app_photos_flickr_field_desc')); }, this);
			this.textField.el.on('mouseout',  function() { controller.indexWindow.fireEvent('statusbar-event-stop'); }, this);
		}, this);
		
		this.controller = Kohive.OS.getController('photos');
		this.controller.on({
		  scope           : this,
		  'image-created' : this.onSuccess
		});
	},
	
	onCreate: function() {
    var form = this.getForm();
		
    // Check if the form is valid
    if (form.isValid()) {
			var value = form.getValues().url;
			
      this.controller.createFromFlickr(value, form);
      this.doLayout();
    };
  },
  
  onSuccess: function() {
    this.textField.setValue('');
  }
});

/**
 * @class Kohive.views.photos.Image
 * @extends Ext.Panel
 * Displays a single photo in the photos application
 */
Kohive.views.photos.Image = Ext.extend(Ext.Panel, {
  
  initComponent: function() {
    /**
     * @property currentImage
     * @type Kohive.models.Image
     * The photo currently being display (defaults to null until a photo is viewed)
     */
    this.currentImage = null;

    /**
     * @property editableListenersAdded
     * @type Boolean
     * We can only add listeners for the Editable ux once we've rendered an image for the first
     * time, so keep track of whether or not these have been set up here.
     */
    this.editableListenersAdded = false;
    
    /**
     * @property hiveId
     * @type Number
     * A reference to the Hive ID this viewer is for.  This is set to the ID of the current hive when this component
     * is created.  It is required because we only want to listen to events from the photos controller when they are
     * fired for this particular hive.  Each hive has its own instance of the photos application so we only listen
     * to events from the relevant hive.
     */
    this.hiveId = Kohive.OS.getCurrentHive().get('hive_id');
    
    /**
     * @property dataview
     * @type Ext.DataView
     * The DataView used internally to render the image
     */
    this.dataview = new Ext.DataView({
      xtype:        'dataview',
      emptyText:    '',
      autoHeight:   true,
      autoScroll:   true,
      itemSelector: '.x-pod',
			overClass:    'x-over',
      // itemSelector: '.x-content',
      tpl:          this.template,
      listeners: {
        scope       : this,
        'click'     : this.viewOnClick
      }
    });
    
    Ext.applyIf(this, {
      layout: 'fit',
			border: false,
      items:  [this.dataview]
    });
    
    this.controller = Kohive.OS.getController('photos');
    
    Kohive.views.photos.Image.superclass.initComponent.apply(this, arguments);
    
    this.initEvents();
    this.initListeners();
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event update
       * Relays this view's Ext.ux.Editable update event.
       * @param {Kohive.model.Image} image The image to update
       * @param {Object} updates An object containing fields and values to update
       */
      
      /**
       * @event delete
       * Fired when the user attempts to delete a given photo
       * @param {Kohive.models.Image} photo The photo the user is trying to delete
       */
      'delete'
    );
  },
  
  /**
   * Sets up listeners to events emitted by the photos controller
   */
  initListeners: function() {
    Kohive.OS.getController('photos').on({
      scope:        this,
      'show-image': this.updateImage,
      'update'    : this.updateImage
    });
    
    this.dataview.on({
      scope   : this,
      'click' : this.onClick
    });
  },
  
  /**
   * Updates this panels' photo to the one provided.  Attached to controller's show-image event
   * @param {Kohive.models.Image} image The image to display
   */
  updateImage: function(image) {
    this.ifCurrentHive(function() {
      if (typeof image == "undefined") return this.hide();

      this.currentImage = image;

      //create a fake store and set it to the DV's new store
      this.dataview.setStore(new Ext.data.Store({
        autoLoad: true,
        proxy:    new Ext.data.MemoryProxy({"image": [image.data]}),
        reader:   Kohive.models.Image.getReader()
      }));
      
      //remove all listeners and add a custom listener to the update event which will update the main images store
      this.dataview.store.un('update', this.updateOriginalStore, this);
      this.dataview.store.on('update', this.updateOriginalStore, this);

      this.setupEditable();
      this.show();
			
			if (image.get('can_edit')) this.makeEditableFieldsHoverable.defer(20, this);
      // if (image.get('can_delete')) this.makeDeleteButtonHoverable.defer(10, this);
			
			var toggle = this.el.child('.x-control-wrap');
			if (toggle) toggle.hide();
			
      this.doLayout.defer(30, this);
    });
  },
  
  /**
   * 
   */
  updateOriginalStore: function(s, record) {
    var store = this.controller.currentStore;
    store.each(function(r) {
      if (r.get('id') == record.get('id')) {
        r.data = record.data;
        r.set('id', record.get('id'));
        this.controller.fireEvent('images-loaded', store, this.controller.currentAlbum[Kohive.OS.getCurrentHiveId()]);
      };
    }, this);
  },
  
  /**
   * Creates a new Ext.ux.Editable instance bound to the dataview
   */
  setupEditable: function() {
    if (this.editableListenersAdded === true) return;
    
    this.editable = new Ext.ux.Editable(this.dataview, {
      view:  this.controller.indexWindow,
      model: Kohive.models.Image,
      clickActions: [
        {
          name:         'title',
          type:         'edit',
          method:       'dblclick',
          allowBlank:   false
        },
        {
          name:         'description',
          value:        'description',
          type:         'edit',
          fieldType:    'textarea',
					forceRefresh: true,
          method:       'dblclick',
          emptyText:    Ext.ux.Translator.translate('app_photos_add_desc')
        },
        {
          name:   'tags',
          type:   'editTags',
          method: 'dblclick'
        }
      ]
    });
    
    this.relayEvents(this.editable, ['update']);
    
    this.editableListenersAdded = true;
  },
	
	makeEditableFieldsHoverable: function() {
		var items  = this.el.select('div'),
				fields = [];
		
		Ext.each(items.elements || [], function(item) {
			var item = Ext.get(item);
			
			Ext.each(item.dom.className.split(' '), function(cls) {
				if (cls.split('-')[0] == "action") {
					fields.push(item);
				};
			}, this);
		}, this);
		
		Ext.each(fields, function(field) {
			field.on('mouseover', field.addClass.createDelegate(field, ['x-over']));
			field.on('mouseout',  field.removeClass.createDelegate(field, ['x-over']));
		}, this);
	},
	
	viewOnClick: function(dataview, index, node, e) {
	  var el     = Ext.get(node),
	      target = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
	  
	  //Delete
	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) this.onDeleteClick();
	},
  
  /**
   * Called when the delete button is clicked - displays a confirmation message, fires a 'delete' event
   * if the user confirms
   */
  onDeleteClick: function() {
    return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('destroy_image_desc', { title: this.currentImage.get('title')}),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
          this.fireEvent('delete', this.currentImage);
        }
      }   
    });
  },
  
  /**
   * Runs a function if the current hive's ID matches this.hiveId
   * @param {Function} fcn A function to run if the current hive matches
   * @param {Object} scope The scope to execute the function in (defaults to the Viewer scope)
   */
  ifCurrentHive: function(fcn, scope) {
    var currentHiveId = Kohive.OS.getCurrentHive().get('hive_id');
    
    if (currentHiveId == this.hiveId) fcn.call(scope || this);
  },
  
  /**
   * 
   */
  onClick: function(dataview, index, node, e) {
    var el     = Ext.get(e.getTarget()),
        record = dataview.getRecord(node);
    
    if (el.hasClass('x-download')) this.onDownload(record);
  },
  
  /**
   * 
   */
  onDownload: function(record) {
    var url = record.get('original_filename');
    window.open(url);
  },
  
  /**
   * @property template
   * @type Ext.XTemplate
   * The template for the image view
   */
  template: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
      '<tpl for=".">',
        '<div id="image-{id}" class="x-pod x-image<tpl if="can_delete"> x-deletable-hover</tpl><tpl if="can_edit"> x-editable-hover</tpl>">',
          '<div class="x-content-wrap">',
            '<div class="x-control-wrap"><div class="x-control"></div></div>',
            '<div class="x-content">',
              //Title
							'<tpl if="title">',
	              '<div class="x-title"><div class="x-title action-edit-title">{title}</div></div>',
							'</tpl>',
							'<tpl if="!title">',
								'<tpl if="can_edit">',
		              '<div class="x-title x-empty"><div class="x-title action-edit-title">' + Ext.ux.Translator.translate('app_add_title') + '</div></div>',
		             '</tpl>',
								'<tpl if="!can_edit">',
									'<div class="x-title x-empty"><div class="x-title">&nbsp;</div></div>',
								'</tpl>',
							'</tpl>',
							
              '<div class="x-extra">',
                //Image
                '<div class="x-image"><div><div class="x-download">{[Ext.ux.Translator.translate("download_original")]}</div><img src="{medium_filename}" /></div></div>',
                
                //Description
                '<tpl if="description">',
                  '<div class="x-description"><div class="action-edit-description">{description_html}</div></div>',
                '</tpl>',
                '<tpl if="!description && can_edit">',
                  '<div class="x-description x-empty"><div class="action-edit-description">' + Ext.ux.Translator.translate('app_add_description') + '</div></div>',
                '</tpl>',
                
                //Tags
                '<tpl if="tags">',
                  '<div class="x-tags">',
                    '<div class="action-editTags-tags">',
                      '<div class="x-form-field-wrap x-trigger-field-wrap">',
                        '<div class="x-form-text x-superboxselect  x-superboxselect-display-btns">',
                          '<ul>',
                            '<tpl for="tags">',
                              '<li class="x-superboxselect-item no-close">{name}</li>',
                            '</tpl>',
                          '</ul>',
                        '</div>',
                      '</div>',
                    '</div>',
                  '</div>',
                '</tpl>',
                '<tpl if="!tags && can_edit">',
                  '<div class="x-tags x-empty"><div class="action-editTags-tags">' + Ext.ux.Translator.translate('app_add_tags') + '</div></div>',
                '</tpl>',
                
                //Other (date + author)
                '<div class="x-other">',
                  '<div class="x-created-on">' + '{author_nickname}, {created_at_date}</div>',
                '</div>',
              '</div>',
              '</div>',
              '<div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div>',
            '</div>',
          '</div>',
        '</div>',
      '</tpl>',
    '</div>'
  )
});

Kohive.views.tasks.Index = function(config) {
  var config = config || {};
  
	this.controller = Kohive.OS.getController('tasks');
	
	config.store = Kohive.models.Task.findAll({
    url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/tasks',
		sortInfo: {
			field:     'created_at',
			direction: 'DESC'
		},
    listeners: {
      'load': {
        scope: this,
        fn: function(store) {
          this.cachedRecords = store;
        }
      }
    }
  });
	
	Ext.applyIf(config, {
		columnPanelConfig: {
      items: [
        {field: 'state',             text: Ext.ux.Translator.translate('state')},
        {field: 'author_nickname',   text: Ext.ux.Translator.translate('author')},
        {field: 'assignee_nickname', text: Ext.ux.Translator.translate('assignee')},
        {field: 'created_at',        text: Ext.ux.Translator.translate('date')},
        {field: 'due',               text: Ext.ux.Translator.translate('due')},
        {field: 'type',              text: Ext.ux.Translator.translate('type')},
        {field: 'priority',          text: Ext.ux.Translator.translate('priority')},
        {field: 'effort',            text: Ext.ux.Translator.translate('effort')},
        {field: 'flagged',           text: Ext.ux.Translator.translate('flagged')},
        {field: 'completed_at',      text: Ext.ux.Translator.translate('completed_on')}
      ]
    },
	  filterPanelConfig: {
      filters: [
        {
          ftype:        'store-helper',
          displayField: 'tags.name',
          valueField:   'tags.name',
          title:        Ext.ux.Translator.translate('tags'),
          iconCls:      'tag',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          displayField: 'author_nickname',
          valueField:   'author_membership_id',
          title:        Ext.ux.Translator.translate('authors'),
          iconCls:      'author',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          displayField: 'assignee_nickname',
          valueField:   'assignee_membership_id',
          title:        Ext.ux.Translator.translate('assignees'),
          iconCls:      'recipients',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          name:         'milestones',
          displayField: 'milestone.title',
          valueField:   'milestone.id',
          title:        Ext.ux.Translator.translate('milestones'),
          iconCls:      'milestone',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          displayField: 'priority_string',
          valueField:   'priority',
          title:        Ext.ux.Translator.translate('priority'),
          iconCls:      'priority',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          displayField: 'effort_string',
          valueField:   'effort',
          title:        Ext.ux.Translator.translate('effort'),
          iconCls:      'effort',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          displayField: 'type_string',
          valueField:   'type',
          title:        Ext.ux.Translator.translate('type'),
          iconCls:      'type',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          displayField: 'flagged_string',
          valueField:   'flagged',
          title:        Ext.ux.Translator.translate('flagged'),
          iconCls:      'flagged',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:        'store-helper',
          displayField: 'state_string',
          valueField:   'state',
          title:        Ext.ux.Translator.translate('state'),
          iconCls:      'state',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        }
        // {
        //   ftype:   'states',
        //   name:    'flagged',
        //   title:   Ext.ux.Translator.translate('flagged'),
        //   iconCls: 'flagged',
        //   listenTo: {
        //     component: this,
        //     events:    ['update-filter-bar']
        //   }
        // },
        // {
        //   ftype:   'states',
        //   name:    'state',
        //   title:   Ext.ux.Translator.translate('state'),
        //   iconCls: 'state',
        //   listenTo: {
        //     component: this,
        //     events:    ['update-filter-bar']
        //   }
        // }
      ],
      listeners: {
        scope            : this,
        'filters-updated': this.onFiltersUpdated
      }
    }
	});
  
  var memberPanel = Kohive.OS.getMemberPanel();
  if(memberPanel) memberPanel.on('membersloaded', function() { this.store.reload(); }, this);
  
  // super
  Kohive.views.tasks.Index.superclass.constructor.call(this, Kohive.models.Task, config);
};
Ext.extend(Kohive.views.tasks.Index, Kohive.views.pod.Index, {
  /**
   * Returns the default dataview config
   * @return {Object} default dataview config
   */
  getDataViewConfig: function() {
    this.store.on('beforeload', function(store) {
      this.setToggledStates(store);
    }, this);
    
    this.store.on('load', function(store) {
      this.onStoreLoad(store);
      this.getToggledStates(store);
		}, this);
		
		this.store.on('add',    this.trimTitles, this);
		this.store.on('remove', this.trimTitles, this);
		this.store.on('update', this.trimTitles, this);
		
    return {
      emptyText:    '<div class="no-results">' + this.getEmptyText() + '</div>',
      itemSelector: '.x-pod',
      overClass:    'x-over',
      tpl:          this.model.dataView,
      store:        this.store,
      listeners: {
        scope       : this,
        'mouseover' : this.viewOnMouseOver,
        'mouseout'  : this.viewOnMouseOut,
        'click'     : this.viewOnClick
      }
    };
  },
  
  onStoreLoad: function (store) {
    this.cachedStore = store;
    this.trimTitles();
    this.viewOnLoad(store);
    this.initTagsStore(store);
    
    //update the original store if the URL hasn't got a ? in it (filtered)
    if (store.proxy.conn.url.split('?').length == 1) {
      this.originalStore = store.data.items;
    };
  },
  
  /**
	 * Called when the windows dataview has been clicked. it has to do a few things:
	 *  - toggle the pod
	 *  - delete stuff
	 * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
   * @param {Integer} index The index of the node in question
   * @param {HTMLElement} node The node in question
   * @param {Ext.EventObject} event The event fired
	 */
	viewOnClick: function(dataview, index, node, e) {
	  var el     = Ext.get(node),
	      target = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
	  
	  //Toggle
	  if (target.hasClass('x-control') || target.parent().hasClass('x-control')) {
	    (record.get('toggled') === true) ? record.set('toggled', false) : record.set('toggled', true);
	  };
	  
	  //Delete
	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) {
      var title = el.child('.x-title div');
      if (title) title = title.dom.innerHTML;
      
      var contentEl = el.child('.x-content-wrap');
      
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('app_' + this.model.modelName.toLowerCase().singularize() + '_destroy_desc', { title: title || null }),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") this.controller.fireAction('destroy', this.controller, [record, dataview.store, contentEl]);
        }
      });
	  };
	  
	  //New Comment, initial
    if (target.hasClass('x-button') || target.parent().hasClass('x-button')) {
      //set the records new_comment field to true
      record.set('new_comment', true);
       
      //focus on the new comment field
      var field = Ext.get('task-' + record.get('id'));
      if (field) this.initField(field, record);
    };
     
    //Toggled comments
    if (target.hasClass('x-toggle-button') || target.parent().hasClass('x-toggle-button')) {
      //set the records new_comment field to true
      record.set('toggled_comments', (record.get('toggled_comments')) ? false : true);
    };
	  
	  //Delete Comment
	  if (target.parent().hasClass('x-delete-comment')) {
	    var el         = Ext.get(target.parent().parent().dom),
	        id         = el.id.split('-')[2] - 1,
	        data       = record.get('comments')[id],
	        comment    = new Kohive.models.Comment(data),
	        controller = Kohive.OS.getController('comments');
	    
	    //set the other_id field to the id being used in the html
	    comment.set('other_id', id + 1);
	    
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('delete_comment_desc'),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") {
            //destroy
            this.showDestroying();
            
            //listen to the controller
            controller.on('destroy',         function(comment) { this.onDestroyCommentSuccess(comment, record); }, this);
            controller.on('destroy-failure', function(comment) { this.onDestroyCommentFailure(); }, this);
            
            //destroy the comment
            controller.destroy(comment);
          };
        }
      });
	  };
	},
	
	/**
	 *  Method called when the dataview store has been loaded.
	 * 
	 * @param {Ext.data.Store} store The store which has been loaded
	 */
	viewOnLoad: function(store) {
	  var dataview = this.sortableDataView.dataview;
	  if (!dataview) return;
	  
	  var nodes = dataview.getNodes();
	  
	  Ext.each(nodes, function(node) {
	    var record = dataview.getRecord(node),
	        el     = Ext.get(node);
	    
	    if (record.get('can_edit')) {
        // editable rollovers
        var items  = el.select('div'),
            fields = [];

        Ext.each(items.elements || [], function(item) {
         var item = Ext.get(item);
 
         Ext.each(item.dom.className.split(' '), function(cls) {
           if (cls.split('-')[0] == "action") fields.push(item);
         }, this);
        }, this);

        Ext.each(fields, function(field) {
          field.on('mouseover', field.addClass.createDelegate(field, ['x-over']));
          field.on('mouseout',  field.removeClass.createDelegate(field, ['x-over']));
        }, this);
        
        // delete comments
        var items  = el.select('.x-comment'),
            fields = [];
        
        Ext.each(items.elements || [], function(item) {
          var item = Ext.get(item);
          fields.push(item);
        }, this);

        Ext.each(fields, function(field) {
          field.on('mouseover', field.addClass.createDelegate(field, ['x-over']));
          field.on('mouseout',  field.removeClass.createDelegate(field, ['x-over']));
        }, this);
      };
    }, this);
	},
	
	/**
	 * 
	 */
	trimTitles: function() {
	  return;
	  
	  //cut down the title
    var nodes = this.sortableDataView.dataview.getNodes();
    Ext.each(nodes, function(node) {
      var el          = Ext.get(node),
          record      = this.sortableDataView.dataview.getRecord(node),
          titleEl     = el.child('.x-title'),
          titleText   = record.get('title'),
          titleTextEl = el.child('.x-title .x-title'),
          width       = Ext.util.TextMetrics.measure(titleTextEl, titleText).width,
          i           = 1,
          trimmed     = false;
      
      // Ext.util.Format.ellipsis
      while (width > (titleEl.getWidth() - 110)) {
        i = i + 1;
        if (i > 10) break;
        
        titleText = Ext.util.Format.substr(titleText, 0, titleText.length - 10);
        width     = Ext.util.TextMetrics.measure(titleTextEl, titleText).width;
        
        trimmed = true;
      };
      
      record.set('title_trimmed', (trimmed) ? titleText + "..." : titleText);
    }, this);
	},
	
	/**
	 * 
	 */
	onDestroyCommentSuccess: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.hideLoading();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	  
	  //delete the comment
	  var el = Ext.get('task-comment-' + comment.get('other_id') + '-' + comment.get('id'));
    if (el) el.fadeOut({remove:true});
    
    //loop through each of the comments on the record and delete the correct one
    var a = [];
    Ext.each(record.get('comments'), function(c) {
      if (c.id != comment.get('id')) a.push(c);
    }, this);
    
    record.set('comments', a);
	},
	
	/**
	 * 
	 */
	onDestroyCommentFailure: function() {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.showFailure();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	},
	
	/**
	 * 
	 */
	initField: function(el, record) {
	  var field  = el.child('.x-field input'),
	      button = el.child('.x-new-comment .x-submit');
	  
	  if (!field || !button) return;
	  
	  //focus the field
	  field.focus();
	  
	  //enable/disable the button on keyup
	  field.on('keyup', function(e) {
	    (field.dom.value.length == 0) ? button.removeClass('x-active') : button.addClass('x-active');
	    
	    //listen for enter key
	    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) {
	      if (button.hasClass('x-active')) this.createComment(record, field.dom.value);
	    };
	    
	    //listen for esc key
	    if (e.getKey() == e.ESC) record.set('new_comment', false);
	  }, this);
	  
	  //enable button click
	  button.on('click', function() { if (button.hasClass('x-active')) this.createComment(record, field.dom.value); }, this);
	},
	
	/**
	 * 
	 */
	createComment: function(record, comment) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //listen for the new comment
	  controller.on('create', this.onCommentCreate, this);
	  
	  //create the comment
	  controller.create(record, comment);
	  
	  //show the loading icon on the window
	  this.showLoading();
	},
	
	/**
	 * 
	 */
	onCommentCreate: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //stop listening to the controller
	  controller.un('create', this.onCommentCreate, this);
	  
	  //get the current membership
	  var membership = Kohive.OS.getCurrentHive();
	  
	  //make the new comment
	  var comment = {
	    created_at:           new Date().format(Kohive.OS.getDefaultDateFormat()),
	    content:              comment.get('content'),
	    author_membership_id: membership.get('id'),
	    author_nickname:      membership.get('id_card').nickname,
	    thumb_filename:       membership.get('id_card').thumb_filename
	  };
	  
	  //add the comment
	  var comments = record.get('comments');
	  comments.unshift(comment);
	  
	  //update a record value to trick extjs into updating the dataview
	  var title = record.get('title');
	  record.set('title', title + '-');
	  record.set('title', title);
	  
	  //remove the new comment field
	  record.set('new_comment', false);
	  
	  //show the success icon on the window
	  this.showSuccess();
	}
});

/**
 * @class Kohive.views.tasks.New
 * @extends Kohive.views.pod.New
 */
Kohive.views.tasks.New = Ext.extend(Kohive.views.pod.New, {
  /**
   * Builds the store for a recipient combo box
   * @return {Ext.data.SimpleStore} The store
   */
  buildMembershipsStore: function() {
    var memberships = [[Ext.ux.Translator.translate('none'), null]];
    
    Ext.each(Kohive.OS.getCurrentHiveMemberships(), function(membership) {
      if (membership.get('state') == 'active') {
        memberships.push([membership.get('id_card').full_name, membership.get('id')]);
      };
    }, this);    
    
    return new Ext.data.SimpleStore({
      fields: ['alias', 'assignee_membership_id'],
      data:   memberships
    });
  },
	
	/**
	 * 
	 */
	updateMembershipStore: function() {
    var membershipsStore = this.buildMembershipsStore();
		this.assigneeField.store = membershipsStore;
	},
	
	/**
	 * 
	 */
	buildFilesStore: function() {
		//get all the files
		var store = Kohive.models.File.findAll({
      url: this.scopedUrl('file_cabinets'),
			listeners: {
				scope : this,
				'load': function(store) {
					//create a array with the default 'none' value
					var files = [[null, Ext.ux.Translator.translate('none')]];

					//loop through each of the store items
					store.each(function(record) {
						files.push([record.get('id'), record.get('title')]);
					}, this);
					
					//create and return the store
					var filesStore = new Ext.data.SimpleStore({
			      fields: ['id', 'title'],
			      data:   files
			    });
					
					Kohive.OS.getController('tasks').files[Kohive.OS.getCurrentHiveId()] = filesStore;
					
					//update the store and set the default value
					this.filesField.store = filesStore;
					this.filesField.setValue(null);
				}
			}
		});
	},
	
	/**
	 * 
	 */
	buildMilestonesStore: function(milestone) {
		//get all the files
		var store = Kohive.models.Milestone.findAll({
      url: this.scopedUrl('milestones'),
			listeners: {
				scope : this,
				'load': function(store) {
				  var simple = [];
				  
					//create a array with the default 'none' value
					var milestones = [[null, Ext.ux.Translator.translate('none'), null]];
          
					//loop through each of the store items
					store.each(function(record) {
						milestones.push([record.get('id'), record.get('title'), record.get('due_at')]);
						simple.push(record.get('id'));
					}, this);
					
					//add the milestone
					if (milestone && simple.indexOf(milestone.get('id')) == -1) milestones.push([milestone.get('id'), milestone.get('title'), milestone.get('due_at')]);
					
					//create and return the store
					var milestonesStore = new Ext.data.SimpleStore({
			      fields: ['id', 'title', 'due_at'],
			      data:   milestones
			    });
					
					Kohive.OS.getController('tasks').milestones[Kohive.OS.getCurrentHiveId()] = milestonesStore;
					
					//update the store and set the default value
          // this.milestonesField.reset();
          if (!this.milestonesField.store) {
            this.milestonesField.store = milestonesStore;
          } else {
            this.milestonesField.reset();
            this.milestonesField.store.removeAll();
            this.milestonesField.store.loadData(milestones);
          };
          this.milestonesField.setValue((milestone) ? milestone.get('id') : null);
				}
			}
		});
	},
  
  /**
   * Set up the component, create references to textarea and counter
   */
  constructor: function(config) {
    var config = config || {};
    
    var membershipsStore = this.buildMembershipsStore();
		
		this.assigneeField = new Ext.form.ComboBox({
      fieldLabel:      Ext.ux.Translator.translate('assigned_to'),
      store:           membershipsStore,
      name:            'assignee_membership_id',
      hiddenName:      'assignee_membership_id',
      triggerAction:   'all',
      mode:            'local',
      displayField:    'alias',
      valueField:      'assignee_membership_id',
			selectOnFocus:   true,
			forceSelection:  true,
      enableKeyEvents: true,
      listeners: {
        'keydown': {
          fn: function(field, e) {
            var key = Ext.EventObject.getKey();
            if (key === 13) e.stopPropagation();
          },
          scope: this
        }
      }
		});
		
		this.buildFilesStore();
		
		this.filesField = new Ext.form.ComboBox({
      fieldLabel:      Ext.ux.Translator.translate('link_to_file'),
      name:            'file_cabinet_id',
      hiddenName:      'file_cabinet_id',
      triggerAction:   'all',
      mode:            'local',
      displayField:    'title',
      valueField:      'id',
			selectOnFocus:   true,
			forceSelection:  true,
      enableKeyEvents: true,
      listeners: {
        'keydown': {
          fn: function(field, e) {
            var key = Ext.EventObject.getKey();
            if (key === 13) e.stopPropagation();
          },
          scope: this
        }
      }
		});
		
		this.dateField = new Ext.form.DateField({
      fieldLabel:    Ext.ux.Translator.translate('due_date'),
      name:          'due',
      emptyText:     Ext.ux.Translator.translate('due_date_empty'),
      format:        'j-M-y',
      minValue:      new Date().format('j-M-y'),
			validateValue: function() { return true; }
		});
		
		this.buildMilestonesStore();
		
		this.milestonesField = new Ext.form.ComboBox({
		  ctCls:           'x-milestones-combo',
      fieldLabel:      Ext.ux.Translator.translate('milestone'),
      name:            'milestone_id',
      hiddenName:      'milestone_id',
      triggerAction:   'all',
      mode:            'local',
      displayField:    'title',
      valueField:      'id',
			selectOnFocus:   true,
			forceSelection:  true,
      enableKeyEvents: true,
      listeners: {
        'keydown': {
          fn: function(field, e) {
            var key = Ext.EventObject.getKey();
            if (key === 13) e.stopPropagation();
          },
          scope: this
        }
      }
		});
		
		this.milestonesNameField = new Ext.form.TextField({
      // name:           'milestone-name',
      fieldLabel:     Ext.ux.Translator.translate('name'),
      listeners: {
        'resize' : function(field) {
          //resize the field
          field.el.setWidth(field.el.getWidth() - 20);
          field.el.parent().setStyle('padding-left', '100px');

          //resize the label
          var label = field.el.parent().parent().child('label');
          label.setWidth(label.getWidth() + 20);
        }
      }
    });
    
    this.milestonesDateField = new Ext.form.DateField({
      fieldLabel:    Ext.ux.Translator.translate('date'),
      emptyText:     Ext.ux.Translator.translate('due_date_empty'),
      format:        'j-M-y',
			validateValue: function() { return true; },
			listeners: {
        'resize' : function(field) {
          //resize the field
          field.el.setWidth(field.el.getWidth() - 20);
          field.el.parent().setWidth(field.el.parent().getWidth() - 20);
          field.el.parent().parent().setStyle('padding-left', '100px');
          
          //resize the label
          var label = field.el.parent().parent().parent().child('label');
          label.setWidth(label.getWidth() + 20);
        }
      }
		});
		
		this.tagsField = new Ext.ux.form.SuperBoxSelect({
      fieldLabel:      Ext.ux.Translator.translate('tags'),
      allowAddNewData: true,
      name:            'tag_list',
      mode:            'local',
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        'enter-key': function(bs) {
          this.onCreate();
        },
        'newitem': function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        }
      }
		});
		
		this.moreInfoPanel = new Ext.Panel({
      name     : 'moreinfo-panel',
      bodyStyle: 'border:0;',
      cls      : 'x-more-info',
      html     :  'More options'
    });
		
    Ext.applyIf(config, {
      defaults: {
        anchor: '100%'
      },
      items: [
// Title
      {
        xtype:      'textfield',
        fieldLabel: Ext.ux.Translator.translate('title'),
        name:       'title',
        allowBlank: false
      },
// Assignee
      this.assigneeField,
      {
        xtype: 'hidden',
        name:  'state',
        value: 'new'
      },
// Description
      {
        xtype:      'textarea',
        fieldLabel: Ext.ux.Translator.translate('description'),
        name:       'body',
				grow:       true,
        enableKeyEvents: true,
        listeners: {
		      'keydown': {
		        fn: function(field, e) {
		          var key = Ext.EventObject.getKey();
		          if (key === 13) {
		            e.stopPropagation();
								if (Ext.EventObject.ctrlKey) this.onCreate();
		          }
		        },
		        scope: this
		      }
		    }
      },
// Tags
      this.tagsField,
      
      {
        xtype     : 'panel',
        html      :  '',
        bodyStyle: 'border-width:0 0 1px 0;margin:0 0 10px 80px;'
      },
      
      // this.moreInfoPanel,
// Due Date
      this.dateField,
// Milestones
      this.milestonesField,
      this.milestonesNameField,
      this.milestonesDateField,
// Link to file
      this.filesField,
// Priority
        {
          xtype:      'radiogroup',
          fieldLabel: Ext.ux.Translator.translate('priority'),
          name:       'priority',
          cls:        'x-priority',
          items: [
            {cls:'low',    name:'priority', inputValue:'1', checked: true},
            {cls:'medium', name:'priority', inputValue:'2'},
            {cls:'high',   name:'priority', inputValue:'3'}
          ],
          onRender: function(ct, position) {
            Ext.form.RadioGroup.superclass.onRender.call(this, ct, position);

            var tempTask = new Ext.util.DelayedTask(function() {
              var inputs = this.el.select('input.x-form-radio');

              inputs.each(function(input) {
                var classes = input.dom.className.split(" ");
                var cls     = classes[classes.length - 2];
                var wrapper = input.parent().parent();
                wrapper.addClass(cls);

                delete classes;
                delete cls;
                delete wrapper;
              }, this);
            }, this);
            tempTask.delay(1000);
          }
        },
// Effort
        {
          xtype:      'radiogroup',
          fieldLabel: Ext.ux.Translator.translate('effort'),
          name:       'effort',
          cls:        'x-effort',
          items: [
            {cls:'low',    name:'effort', inputValue:'1', checked: true},
            {cls:'medium', name:'effort', inputValue:'2'},
            {cls:'high',   name:'effort', inputValue:'3'}
          ],
          onRender: function(ct, position) {
            Ext.form.RadioGroup.superclass.onRender.call(this, ct, position);

            var tempTask = new Ext.util.DelayedTask(function() {
              var inputs = this.el.select('input.x-form-radio');

              inputs.each(function(input) {
                var classes = input.dom.className.split(" ");
                var cls     = classes[classes.length - 2];
                var wrapper = input.parent().parent();
                wrapper.addClass(cls);

                delete classes;
                delete cls;
                delete wrapper;
              }, this);
            }, this);
            tempTask.delay(1000);
          }
        },
// Type
        {
          xtype:      'radiogroup',
          fieldLabel: Ext.ux.Translator.translate('type'),
          name:       'type',
          cls:        'x-type',
          items: [
            {cls:'null',    name:'type', inputValue:'0', checked: true},
            {cls:'cube',    name:'type', inputValue:'1'},
            {cls:'star',    name:'type', inputValue:'2'},
            {cls:'spider',  name:'type', inputValue:'3'},
            {cls:'bug',     name:'type', inputValue:'4'},
            {cls:'brush',   name:'type', inputValue:'5'},
            {cls:'console', name:'type', inputValue:'6'},
            {cls:'tool',    name:'type', inputValue:'7'},
            {cls:'colors',  name:'type', inputValue:'8'}
          ],
          onRender: function(ct, position) {
            Ext.form.RadioGroup.superclass.onRender.call(this, ct, position);

            var tempTask = new Ext.util.DelayedTask(function() {
              var inputs = this.el.select('input.x-form-radio');

              inputs.each(function(input) {
                var classes = input.dom.className.split(" ");
                var cls     = classes[classes.length - 2];
                var wrapper = input.parent().parent();
                wrapper.addClass(cls);

                delete classes;
                delete cls;
                delete wrapper;
              }, this);
            }, this);
            tempTask.delay(1000);
          }
        }
      ]
    });

    Kohive.views.tasks.New.superclass.constructor.call(this, Kohive.models.Task, config);
		
		this.on('render', function() {
			this.assigneeField.setValue(null);
		}, this);
		
		this.on('expand', function() {
		  this.initTypeRollovers();
		  this.initMilestones();
      // this.initMoreInfo();
		}, this);
		
		//update the membership store when the members change
		var memberPanel = Kohive.OS.getMemberPanel();
		memberPanel.on('membersloaded', this.updateMembershipStore, this);
  },

	/**
   * Called when the save button is clicked or CTRL + s pressed.  By default this simply fires
   * the associated controller's 'update' action, passing this.getForm() as the sole argument
   */
  onCreate: function() {
    var form = this.getForm();
    
		//get the value form the date field
		var date = (this.dateField.getValue()) ? this.dateField.getValue() : this.dateField.getRawValue();
		
		//tags
    form.items.each(function(item) {
      if (item.name == "tag_list") {
        var val = item.el.dom.value;
        if (!val) return;
        
        item.view.clearSelections();
        item.collapse();
        item.setRawValue('');
        item.fireEvent('newitem', item, val);
      };
    }, this);
		
    // Check if the form is valid
    if (form.isValid()) {
      // this.onCreateMilestonePress(true);
      
      this.controller.fireAction('create', null, [form, date]);
      this.doLayout();
    };
  },
	
	/**
	 * Sets up listeners to the new milestone button and field, and sets them up to actually work
	 * on the front end.
	 */
	initMilestones: function() {
	  //add the button toe the field
    this.milestonesField.el.insertHtml('afterEnd', '<div class="x-new-btn"></div>');
	  
	  var form = this.getForm(),
        btn  = this.el.child('.x-new-btn'),
        milestonesNameField = this.milestonesNameField.el.parent().parent(),
        milestonesDateField = this.milestonesDateField.el.parent().parent().parent();
    
    //update the field
    milestonesNameField.enableDisplayMode();
    milestonesDateField.enableDisplayMode();
    milestonesNameField.hide();
    milestonesDateField.hide();
    
    this.milestoneVisible = false;
    
    //add listeners to hide the fields
    this.milestonesNameField.on('keypress', function(e) {
      if (e.getKey() == e.ESC) {
        milestonesNameField.hide();
        milestonesDateField.hide();
      };
    }, this);
    this.milestonesDateField.on('keypress', function(e) {
      if (e.getKey() == e.ESC) {
        milestonesNameField.hide();
        milestonesDateField.hide();
      };
    }, this);
    
    //add a click listener to the field which shows/hides the button + input field
    btn.on('click', this.onCreateMilestonePress, this);
    
    //hide the extra fields by default
    // this.cp = new Ext.state.CookieProvider();
    // var state = (this.cp.readCookies()['tasks-extra-fields'] === true || this.cp.readCookies()['tasks-extra-fields'] === false) ? this.cp.readCookies()['tasks-extra-fields'] : false;
    // if (!state) this.toggleMoreInfo();
	},
	
	milestoneVisible: false,
	
	/**
   * TODO: A hacky method to prepend the /memberships/:membership_id/ segment to the front of an AJAX
   * request url.  This really ought to be handled by the model, so factor it out of here when possible
   * @param {String} url The url to append to the memberships scope
   * @return {String} The fully scoped url
   */
  scopedUrl: function(url) {
    return String.format("/memberships/{0}/{1}", Kohive.OS.getCurrentHive().get('id'), url);
  },
  
  onCreateMilestonePress: function(creating) {
    var form          = this.getForm(),
        btn           = this.el.child('.x-new-btn') || this.el.child('.x-submit-btn'),
        milestonesNameField = this.milestonesNameField.el.parent().parent(),
        milestonesDateField = this.milestonesDateField.el.parent().parent().parent();
    
    if (milestonesNameField.isVisible()) {
      if (this.milestonesNameField.getValue() == "" && this.milestonesDateField.getValue() == "") {
        milestonesNameField.hide();
        milestonesDateField.hide();
        
        this.milestoneVisible = false;
        
        btn.addClass('x-new-btn');
        btn.removeClass('x-submit-btn');
      } else {
        this.onCreateMilestone(this.milestonesNameField.getValue(), this.milestonesDateField.getValue());
      };
    } else {
      if (creating === true) return;
      
      milestonesNameField.show();
      milestonesDateField.show();
      this.milestonesNameField.setValue('');
      this.milestonesDateField.setValue('');
      this.milestonesNameField.focus();
      
      this.milestoneVisible = true;
      
      btn.removeClass('x-new-btn');
      btn.addClass('x-submit-btn');
    };
  },
  
  /**
   * 
   */
  onCreateMilestone: function(title, due_at) {
    var milestone = new Kohive.models.Milestone({
      title:  title,
      due_at: due_at
    });
    
    milestone.save({
      url:     Kohive.OS.scopedUrl('milestones'),
      scope:   this,
      success: function(record, response) {
        //create the comment from the response
        var milestone = new Kohive.models.Milestone(Ext.decode(response.responseText));
        
        this.onCreateMilestoneSuccess(milestone);
      },
      failure: this.onCreateMilestoneFailure
    });
  },
  
  /**
   * 
   */
  onCreateMilestoneSuccess: function(record) {
    this.buildMilestonesStore(record);
    
    var form          = this.getForm(),
        btn           = this.el.child('.x-new-btn') || this.el.child('.x-submit-btn'),
        milestonesNameField = this.milestonesNameField.el.parent().parent(),
        milestonesDateField = this.milestonesDateField.el.parent().parent().parent();
    
    milestonesNameField.hide();
    milestonesDateField.hide();
    
    btn.addClass('x-new-btn');
    btn.removeClass('x-submit-btn');
  },
  
  /**
   * 
   */
  onCreateMilestoneFailure: function() {
    Ext.Msg.alert('', Ext.ux.Translator.translate('something_wrong'));
  },
  
  typedInited: false,
  
  /**
   * 
   */
  initTypeRollovers: function() {
    if (this.typedInited) return;
    else this.typedInited = true;
    
    var wrapper    = this.el.child('.x-form-field.x-type'),
        types      = wrapper.select('img.x-form-radio'),
        controller = Kohive.OS.getController('tasks'),
        win        = controller.indexWindow;
    
    types.each(function(type) {
      var input       = type.parent().child('input'),
          name        = input.dom.className.replace('x-form-radio', '').replace('x-form-field', '').replace('x-hidden', '').replace('   ', '').replace('  ', '').replace(' ', ''),
          translation = this.getTypeTranslation(name);
      
      type.on('mouseover', function() { win.fireEvent('statusbar-event-start', Ext.ux.Translator.translate(translation)); }, this);
      type.on('mouseout',  function() { win.fireEvent('statusbar-event-stop'); }, this);
    }, this);
  },
  
  getTypeTranslation: function(type) {
    var name = "";
    
    switch (type) {
      case "null":
        name = "app_tasks_null";
        break;
      
      case "cube":
        name = "app_tasks_component";
        break;
      
      case "star":
        name = "app_tasks_feature";
        break;
      
      case "spider":
        name = "app_tasks_critical_bug";
        break;
      
      case "bug":
        name = "app_tasks_bug";
        break;
        
      case "brush":
        name = "app_tasks_chore";
        break;
      
      case "console":
        name = "app_tasks_admin";
        break;
      
      case "tool":
        name = "app_tasks_tweak";
        break;
      
      case "colors":
        name = "app_tasks_design";
        break;
    };
    
    return name;
  },
  
  /**
   * Sets up the more info panel to accept clicks for hiding/showing the extra fields
   */
  initMoreInfo: function() {
    this.moreInfoPanel.el.on('click', this.toggleMoreInfo, this);
  },
  
  isVisible: true,
  
  /**
   * 
   */
  toggleMoreInfo: function() {
    var items     = this.form.el.parent().select('form > .x-form-item'),
        firstItem = Ext.get(items.elements[4]);
    
    // more than the 3rd one down
    Ext.each(items.elements, function(dom, i) {
      if (i > 3) {
        var el = Ext.get(dom);
        
        el.enableDisplayMode();
        el.setVisible(!this.isVisible);
      };
    }, this);
    
    // this.doLayout();
    
    this.isVisible = !this.isVisible;
    
    this.moreInfoPanel.el.update((this.isVisible) ? 'less options' : 'more options');
    if (this.isVisible) this.moreInfoPanel.el.addClass('x-up');
    else this.moreInfoPanel.el.removeClass('x-up');
    
    this.cp.setCookie('tasks-extra-fields', this.isVisible);
    
    var milestonesNameField = this.milestonesNameField.el.parent().parent(),
        milestonesDateField = this.milestonesDateField.el.parent().parent().parent();
    
    var milestoneVisible = (this.isVisible == false) ? false : this.milestoneVisible;
    
    milestonesNameField.setVisible(milestoneVisible);
    milestonesDateField.setVisible(milestoneVisible);
    
    this.milestoneVisible = milestoneVisible;
  }
});

Kohive.views.stickies.Index = function(config) {
  var config = config || {};
  
  this.controller = this;
  
  // super
  Kohive.views.stickies.Index.superclass.constructor.call(this, Kohive.models.Sticky, config);
  
  config.store = Kohive.models.Sticky.findAll({
    url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/' + Kohive.models.Sticky.urlName,
		sortInfo: {
			field:     'created_at',
			direction: 'DESC'
		},
    listeners: {
      'load': {
        scope: this,
        fn: this.onStoreLoad
      }
    }
  });
  
  Ext.applyIf(config, {
    hbar: [
      {
        text:    Ext.ux.Translator.translate('nnew'),
        iconCls: 'new',
        xtype:   'header_toolbar_button',
        handler: this.toggleNewForm,
        scope:   this
      },
      {
        text:    Ext.ux.Translator.translate('refresh'),
        iconCls: 'refresh',
        xtype:   'header_toolbar_button',
        handler: function() {
          this.controller.fireAction('refresh', this.controller);
        },
        scope: this
      },
			'->',
      {
        xtype: 'searchfield',
				listeners: {
					scope: this,
					'keyup' : this.searchStore
				}
      }
    ],
		filterPanelConfig: {
      filters: [
        {
          ftype:        'store-helper',
          displayField: 'author_nickname',
          valueField:   'author_membership_id',
          title:        Ext.ux.Translator.translate('authors'),
          iconCls:      'author',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        }
      ],
      listeners: {
        scope            : this,
        'filters-updated': this.onFiltersUpdated
      }
    }
  });
};
Ext.extend(Kohive.views.stickies.Index, Kohive.views.pod.Index, {
  onStoreLoad: function(store) {
    this.cachedRecords = store;
    this.addDragger();
  },
  
  /**
   * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
	 * @param {Integer} index The index of the node in question
	 * @param {HTMLElement} node The node in question
	 * @param {Ext.EventObject} event The event fired
	 */
	viewOnMouseOver: function(dataview, index, node, el) {
	  var el     = Ext.get(node),
	      record = dataview.getRecord(node);
	  
	  //if you can edit show the dragger tooltip
    if (record.get('can_drag')) {
      this.fireEvent('statusbar-event-start', Ext.ux.Translator.translate('app_statusbar_drag_to_desktop'));
    };
	},
	
	/**
	 * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
 	 * @param {Integer} index The index of the node in question
 	 * @param {HTMLElement} node The node in question
 	 * @param {Ext.EventObject} event The event fired
	 */
	viewOnMouseOut: function(dataview, index, node, el) {
	  var el     = Ext.get(node),
	      record = dataview.getRecord(node);
	  
    this.fireEvent('statusbar-event-stop');
	},
  
  /**
   * 
   */
  addDragger: function() {
    var dataview = this.sortableDataView.dataview,
        nodes    = (dataview) ? dataview.getNodes() : [],
        view     = this;
    
    Ext.each(nodes || [], function(node) {
      var el     = Ext.get(node).child('.x-stamp'),
          record = dataview.getRecord(node);
      
      if (!el || !record.get('can_drag')) return;
      
      //Add drag functionality to the member
      el.dd = new Ext.dd.DragSource(el, {
  			scroll:  false,
        ddGroup: 'desktop-stickies',
        
        /**
         * 
         */
        getDragData: function() {
          return {
            sticky: record,
            view:   view
          };
        },
  			
        onDrag: function(e) {
          var dragEl = Ext.get(this.getDragEl());
          var el = Ext.get(this.getEl()).parent();
          
          if (!dragEl || !el) return;
          
          dragEl.applyStyles({'z-index':35100});
          dragEl.update(el.dom.innerHTML);
          dragEl.addClass('x-dragger');
          dragEl.addClass(el.dom.className);
        },
        
        autoOffset: function(x, y) {
          if (!el) return;
          var memberXY = el.getXY();

          xDelta = x - memberXY[0];
          yDelta = y - memberXY[1];

          this.setDelta(xDelta, yDelta);
        },
        
        // onValidDrop: function() {
        //   console.log('onValidDrop');
        //   
        //   Ext.dd.DragSource.prototype.onValidDrop.apply(this, arguments);
        // },
        // 
        // onInvalidDrop: function() {
        //   console.log('onInvalidDrop');
        //   
        //   Ext.dd.DragSource.prototype.onInvalidDrop.apply(this, arguments);
        // },
        
        hideProxy: function() {
          this.proxy.hide(true);  
          this.proxy.reset(true);
          this.dragging = false;
        },
        
        afterRepair: function() {
          this.dragging = false;
        }
      });
    }, this);
    
    this.controller.on('refresh', this.onStoreLoad, this);
  }
});

Kohive.views.stickies.New = function(config) {
  var config = config || {};
	
	Ext.applyIf(config, {
		items: [
			{
		    xtype:      'textarea',
		    fieldLabel: Ext.ux.Translator.translate('message'),
		    name:       'body',
		    allowBlank: false,
		    width:      '98%',
				grow:       true,
				growMin:    30,
		    height:     30,
		    maxLength:  265,
		    enableKeyEvents: true,
		    listeners: {
		      'keydown': {
		        fn: function(field, e) {
		          var key = Ext.EventObject.getKey();
		          if (key === 13) {
		            e.stopPropagation();
								if (Ext.EventObject.ctrlKey) this.onCreate();
		          }
		        },
		        scope: this
		      }
		    }
		  },
		  {
		    xtype:      'radiogroup',
		    fieldLabel: Ext.ux.Translator.translate('colour'),
		    name:       'color',
		    items: [
		      {cls:'yellow', name:'color', inputValue:'yellow', checked: true},
		      {cls:'orange', name:'color', inputValue:'orange'},
		      {cls:'green',  name:'color', inputValue:'green'},
		      {cls:'blue',   name:'color', inputValue:'blue'},
		      {cls:'purple', name:'color', inputValue:'purple'},
		      {cls:'pink',   name:'color', inputValue:'pink'}
		    ],
		    onRender: function(ct, position) {
		      Ext.form.RadioGroup.superclass.onRender.call(this, ct, position);

		      var tempTask = new Ext.util.DelayedTask(function() {
		        var inputs = this.el.select('input.x-form-radio');

		        inputs.each(function(input) {
		          var classes = input.dom.className.split(" ");
		          var cls     = classes[classes.length - 2];
		          var wrapper = input.parent().parent();
		          wrapper.addClass(cls);

		          delete classes;
		          delete cls;
		          delete wrapper;
		        }, this);
		      }, this);
		      tempTask.delay(1000);
		    }
		  },
		  {    
		    xtype:      'radiogroup',
		    fieldLabel: Ext.ux.Translator.translate('stamp'),
		    name:       'stamp',
		    items: [
		      {cls:'star',        name:'stamp', inputValue:'star'},
		      {cls:'fire',        name:'stamp', inputValue:'fire'},
		      {cls:'clover',      name:'stamp', inputValue:'clover'},
		      {cls:'exclamation', name:'stamp', inputValue:'exclamation'},
		      {cls:'heart',       name:'stamp', inputValue:'heart'},
		      {cls:'kiss',        name:'stamp', inputValue:'kiss'}
		    ],
		    onRender: function(ct, position) {
		      Ext.form.RadioGroup.superclass.onRender.call(this, ct, position);

		      var tempTask = new Ext.util.DelayedTask(function() {
		        var inputs = this.el.select('input.x-form-radio');

		        inputs.each(function(input) {
		          var classes = input.dom.className.split(" ");
		          var cls     = classes[classes.length - 2];
		          var wrapper = input.parent().parent();
		          wrapper.addClass(cls);

		          delete classes;
		          delete cls;
		          delete wrapper;
		        }, this);
		      }, this);
		      tempTask.delay(1000);
		    }
		  }
		],
	  submitButtonConfig: {
			text: Ext.ux.Translator.translate('post')
		}
	});
  
  // super
  Kohive.views.stickies.New.superclass.constructor.call(this, Kohive.models.Sticky, config);
};
Ext.extend(Kohive.views.stickies.New, Kohive.views.pod.New);

// /**
//  * @class Kohive.views.Stickies.New
//  * @extends Ext.form.FormPanel
//  * Represents a new stickies form
//  */
// Kohive.views.Stickies.New = function(config) {
//   var config = config || {};
//   
//   this.addEvents(
//     /**
//      * @event create
//      * Fires when the 'Add' button is clicked indicating the user wants to save the new MicroBlog
//      * @param {Ext.form.FormPanel} this The form panel
//      */
//     'create'
//   );
//   
//   this.priorities = new Ext.data.SimpleStore({
//     fields: ['priority'],
//     data : [
//       ['low'],
//       ['medium'],
//       ['high'],
//     ]
//   });
//   
//   Ext.applyIf(config, {
//     title:  Ext.ux.Translator.translate('app_stickies_new'),
//     collapsed: true
//   });
//  
//   Kohive.views.Stickies.New.superclass.constructor.call(this, config);
// };
// Ext.extend(Kohive.views.Stickies.New, Kohive.FormPanel);
// 
// Ext.reg('kohive_stickies_new', Kohive.views.Stickies.New);

Kohive.views.microblogs.Index = function(config) {
  var config = config || {};
  
	this.controller = Kohive.OS.getController('microblogs');
	
	config.store = Kohive.models.MicroBlog.findAll({
    url: '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/microblogs',
		sortInfo: {
			field:     'created_at',
			direction: 'DESC'
		},
    listeners: {
      'load': {
        scope: this,
        fn: function(store) {
          this.cachedRecords = store;
        }
      }
    }
  });
	
  Ext.applyIf(config, {
    // hasTags: true,
    columnPanelConfig: {
      items: [
        {field: 'author_nickname', text: Ext.ux.Translator.translate('creator')},
        {field: 'created_at',      text: Ext.ux.Translator.translate('date')   }
      ]
    },
    filterPanelConfig: {
      filters: [
        {
          ftype:        'store-helper',
          displayField: 'author_nickname',
          valueField:   'author_membership_id',
          title:        Ext.ux.Translator.translate('authors'),
          iconCls:      'author',
          view:         this,
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        },
        {
          ftype:   'hashs',
          listenTo: {
            component: config.store,
            events:    ['load', 'update']
          }
        }
      ],
      listeners: {
        scope            : this,
        'filters-updated': this.onFiltersUpdated
      }
    }
  });
  
  // super
  Kohive.views.microblogs.Index.superclass.constructor.call(this, Kohive.models.MicroBlog, config);
  
  this.store.on('load', this.onStoreLoad, this);
  
  this.initListeners();
  
  var membership = Kohive.OS.getCurrentHive();
  if (membership) {
    if (membership.get('sync_with_twitter') === true) {
      this.syncWithTwitter = true;
    }
    
    this.updateTwitterButtonText();
  };
};
Ext.extend(Kohive.views.microblogs.Index, Kohive.views.pod.Index, {
  // Probably not best solution, but for now if an app has a onstoreload method, we init the filterUpdateTask in Kohive.Window
	onStoreLoad: Ext.emptyFn,
	
	/**
   * Listens to controller events and takes action as appropriate
   */
  initListeners: function() {
    Kohive.OS.getController('microblogs').on({
      scope                   :  this,
     // 'create'                 : function() {this.store.reload();},
     'twitter-pin-saved'      : function() {this.store.reload();},
     'turned-off-twitter-sync': this.onTurnedOffTwitterSync,
     'twitter-authorized'     : this.onTwitterAuthorized
    });
  },
	
	/**
   * Updates the text on the twitter button depending on whether or not syncing is currently enabled
   */
  updateTwitterButtonText: function() {
    var text = String.format(Ext.ux.Translator.translate('twitter_sync') + " ({0})", this.syncWithTwitter ? Ext.ux.Translator.translate('on') : Ext.ux.Translator.translate('off'));
    this.twitterButton.setText(text);
   
   this.twitterButton.removeClass(this.syncWithTwitter ? 'off' : 'on');
   this.twitterButton.addClass(this.syncWithTwitter ? 'on' : 'off');
  },
  
  /**
   * Called when the twitter sync button is clicked
   */
  toggleTwitterSync: function() {
    var idCard = new Kohive.models.IdCard(Kohive.OS.getIdCardData());
    
    if (!this.syncWithTwitter) {
      var membership = Kohive.OS.getCurrentHive(); // getCurrentHive actually returns a membership...
      
      var twitter_login_url = String.format("/memberships/{0}/twitter/login", membership.get('id'));
      // Added timestamp to open a new tab/window and to put focus on it if the tab from the previous authorization is still open
      window.open(twitter_login_url, "twitter_login_url" + (new Date()).getTime());
      
      Ext.Msg.prompt('Twitter', 'PIN:', function(btn, pin){
        if (btn == 'ok'){
          this.fireEvent('entered-twitter-pin', pin);
        }
      }, this);
    } else {
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('remove_twitter_sync_desc'),
        buttons: {yes: Ext.ux.Translator.translate('turn_off'), no: Ext.ux.Translator.translate('leave_on')},
        scope:   this,
        fn:      function(msg) {
          if (msg == "yes") this.fireEvent('turn-off-twitter-sync');
        }
      });
    }
  },
  
  onTurnedOffTwitterSync: function() {
    this.syncWithTwitter = false;
    this.updateTwitterButtonText();
  },
  
  onTwitterAuthorized: function () {
    this.syncWithTwitter = true;
    this.updateTwitterButtonText();
    
    this.store.reload();
  },
	
	/**
   * Called whenever the user changes the filters applied in the filter bar. Fires the 'filter' event
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  onFiltersUpdated: function(filters, hashs) {
   // TODO do this the proper way, until then, do without events..
   if (!this.store) return;
   
   //clear all filters first
   this.store.clearFilter();
   
   if (hashs) {
     if (!filters || !filters.hashs) return;
     if (filters.hashs.length == 0) return;
     
     var str = "";
     Ext.each(filters.hashs, function(h, i) {
       str += h;
       if (i != filters.hashs.length - 1) str += " ";
     }, this);
     
     this.searchStore(str, true);
   } else {
     //for some reason passing the params option to findAll isn't working, so build url manually :/
     var url        = Kohive.OS.scopedUrl(this.model.urlName || this.model.modelName.toLowerCase().pluralize()),
         conditions = [];

     //     tags       = filters.tags,
     //     authors    = filters.authors,
     //     recipients = filters.assignee_membership_id,
     //     priority   = filters.priority,
     //     effort     = filters.effort,
     //     type       = filters.type,
     //     flagged    = filters.flagged,
     //     state      = filters.state,
     //     milestones = filters.milestones,
     //     conditions = [];
     // 
     // if (tags && tags.length > 0)             conditions.push(String.format("tags={0}", tags.join(',')));
     // if (authors && authors.length > 0)       conditions.push(String.format("membership_ids={0}", authors.join(',')));
     // if (recipients && recipients.length > 0) conditions.push(String.format("recipient_membership_ids={0}", recipients.join(',')));
     // if (priority && priority.length > 0)     conditions.push(String.format("priority={0}", priority.join(',')));
     // if (effort && effort.length > 0)         conditions.push(String.format("effort={0}", effort.join(',')));
     // if (effort && effort.length > 0)         conditions.push(String.format("effort={0}", effort.join(',')));
     // if (type && type.length > 0)             conditions.push(String.format("type={0}", type.join(',')));
     // if (flagged && flagged.length > 0)       conditions.push(String.format("flagged={0}", flagged.join(',')));
     // if (state && state.length > 0)           conditions.push(String.format("state={0}", state.join(',')));
     // if (milestones && milestones.length > 0) conditions.push(String.format("milestones={0}", milestones.join(',')));

     //loop through each of the filters and add them to the conditions
     for (filter in filters) {
       if (filters[filter].length > 0) conditions.push(String.format(filter + "={0}", filters[filter].join(',')));
     };

		//update the stores url + reload the store
		if (conditions.length > 0) this.store.proxy.conn.url = url + '?' + conditions.join("&");
		else this.store.proxy.conn.url = url;

    this.store.load();
  };
  },
	
	/**
   * Overrides and adds to the default header toolbar buttons
   * @return {Array} The header buttons
   */
  getDefaultHBar: function() {
    /**
     * @property twitterButton
     * @type Ext.Button
     * The Twitter Sync toggle button
     */
    this.twitterButton = new Ext.ux.HeaderToolbarButton({
      text:    Ext.ux.Translator.translate('twitter_sync'),
      iconCls: 'twitter',
      scope:   this,
      handler: this.toggleTwitterSync
    });
    
   return [
     {
        text:    Ext.ux.Translator.translate('nnew'),
        iconCls: 'new',
        xtype:   'header_toolbar_button',
        handler: this.toggleNewForm,
        scope:   this
      },
      {
        text:    Ext.ux.Translator.translate('refresh'),
        iconCls: 'refresh',
        xtype:   'header_toolbar_button',
        handler: this.controller.fireAction.createDelegate(this.controller, ['refresh', this.controller])
      },
      {
        text:    Ext.ux.Translator.translate('toggle_all'),
        iconCls: 'toggle-all',
        xtype:   'header_toolbar_button',
        handler: this.toggleAll,
        scope:   this
      },
      this.twitterButton,
      '->',
     {
        xtype: 'searchfield',
       listeners: {
         scope: this,
         'keyup' : this.searchStore
       }
      }
   ];
  },
  
  /**
	 * Called when the windows dataview has been clicked. it has to do a few things:
	 *  - toggle the pod
	 *  - delete stuff
	 * 
	 * @param {Ext.DataView} dataview The dataview which has all the records
   * @param {Integer} index The index of the node in question
   * @param {HTMLElement} node The node in question
   * @param {Ext.EventObject} event The event fired
	 */
	viewOnClick: function(dataview, index, node, e) {
	  var el     = Ext.get(node),
	      target = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
	  
	  //Toggle
	  if (target.hasClass('x-control') || target.parent().hasClass('x-control')) {
	    (record.get('toggled') === true) ? record.set('toggled', false) : record.set('toggled', true);
	  };
	  
	  //Delete
	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) {
      var title = el.child('.x-title div');
      if (title) title = title.dom.innerHTML;
      
      var contentEl = el.child('.x-content-wrap');
      
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('app_' + this.model.modelName.toLowerCase().singularize() + '_destroy_desc', { title: title || null }),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") this.controller.fireAction('destroy', this.controller, [record, dataview.store, contentEl]);
        }
      });
	  };
	  
	  //New Comment, initial
    if (target.hasClass('x-button') || target.parent().hasClass('x-button')) {
      //set the records new_comment field to true
      record.set('new_comment', true);
       
      //focus on the new comment field
      var field = Ext.get('microblog-' + record.get('id'));
      if (field) this.initField(field, record);
    };
     
    //Toggled comments
    if (target.hasClass('x-toggle-button') || target.parent().hasClass('x-toggle-button')) {
      //set the records new_comment field to true
      record.set('toggled_comments', (record.get('toggled_comments')) ? false : true);
    };
	  
	  //Delete Comment
	  if (target.parent().hasClass('x-delete-comment')) {
	    var el         = Ext.get(target.parent().parent().dom),
	        id         = el.id.split('-')[2] - 1,
	        data       = record.get('comments')[id],
	        comment    = new Kohive.models.Comment(data),
	        controller = Kohive.OS.getController('comments');
	    
	    //set the other_id field to the id being used in the html
	    comment.set('other_id', id + 1);
	    
      return Ext.Msg.show({
        title:   '',
        msg:     Ext.ux.Translator.translate('delete_comment_desc'),
        buttons: {yes: Ext.ux.Translator.translate('ddelete'), no: Ext.ux.Translator.translate('keep')},
        scope:   this,
        fn:      function(btn) {
          if (btn == "yes") {
            //destroy
            this.showDestroying();
            
            //listen to the controller
            controller.on('destroy',         function(comment) { this.onDestroyCommentSuccess(comment, record); }, this);
            controller.on('destroy-failure', function(comment) { this.onDestroyCommentFailure(); }, this);
            
            //destroy the comment
            controller.destroy(comment);
          };
        }
      });
	  };
	},
	
	/**
	 * 
	 */
	onDestroyCommentSuccess: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.hideLoading();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	  
	  //delete the comment
	  var el = Ext.get('microblog-comment-' + comment.get('other_id') + '-' + comment.get('id'));
    if (el) el.fadeOut({remove:true});
    
    //loop through each of the comments on the record and delete the correct one
    var a = [];
    Ext.each(record.get('comments'), function(c) {
      if (c.id != comment.get('id')) a.push(c);
    }, this);
    
    record.set('comments', a);
	},
	
	/**
	 * 
	 */
	onDestroyCommentFailure: function() {
	  var controller = Kohive.OS.getController('comments');
	  
	  this.showFailure();
	  
	  //stop listening to the controller
	  controller.un('destroy',         this.onDestroyCommentSuccess, this);
	  controller.un('destroy-failure', this.onDestroyCommentFailure, this);
	},
	
	/**
	 * 
	 */
	initField: function(el, record) {
	  var field  = el.child('.x-field input'),
	      button = el.child('.x-new-comment .x-submit');
	  
	  if (!field || !button) return;
	  
	  //focus the field
	  field.focus();
	  
	  //enable/disable the button on keyup
	  field.on('keyup', function(e) {
	    (field.dom.value.length == 0) ? button.removeClass('x-active') : button.addClass('x-active');
	    
	    //listen for enter key
	    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) {
	      if (button.hasClass('x-active')) this.createComment(record, field.dom.value);
	    };
	    
	    //listen for esc key
	    if (e.getKey() == e.ESC) record.set('new_comment', false);
	  }, this);
	  
	  //enable button click
	  button.on('click', function() { if (button.hasClass('x-active')) this.createComment(record, field.dom.value); }, this);
	},
	
	/**
	 * 
	 */
	createComment: function(record, comment) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //listen for the new comment
	  controller.on('create', this.onCommentCreate, this);
	  
	  //create the comment
	  controller.create(record, comment);
	  
	  //show the loading icon on the window
	  this.showLoading();
	},
	
	/**
	 * 
	 */
	onCommentCreate: function(comment, record) {
	  var controller = Kohive.OS.getController('comments');
	  
	  //stop listening to the controller
	  controller.un('create', this.onCommentCreate, this);
	  
	  //get the current membership
	  var membership = Kohive.OS.getCurrentHive();
	  
	  //make the new comment
	  var comment = {
	    created_at:           new Date().format(Kohive.OS.getDefaultDateFormat()),
	    content:              comment.get('content'),
	    author_membership_id: membership.get('id'),
	    author_nickname:      membership.get('id_card').nickname,
	    thumb_filename:       membership.get('id_card').thumb_filename
	  };
	  
	  //add the comment
	  var comments = record.get('comments');
	  comments.unshift(comment);
	  
	  //update a record value to trick extjs into updating the dataview
	  var title = record.get('title');
	  record.set('title', title + '-');
	  record.set('title', title);
	  
	  //remove the new comment field
	  record.set('new_comment', false);
	  
	  //show the success icon on the window
	  this.showSuccess();
	}
});

/**
 * @class Kohive.views.microblogs.New
 * @extends Kohive.views.pod.New
 * New Microblog Pod with character limit enforcement
 */
Kohive.views.microblogs.New = Ext.extend(Kohive.views.pod.New, {
  /**
   * @property maxLength
   * @type Number
   * The maximum length of a microblog message (defaults to 140)
   */
  maxLength: 140,
  
  /**
   * @property warningCount
   * @type Number
   * The number of characters remaining at which the counter obtains a 'warning' CSS class
   */
  warningCount: 10,
  
  /**
   * @property warningClass
   * @type String
   * The CSS class to apply to the counter when the warning level is reached
   */
  warningClass: 'x-counter-warning',
  
  /**
   * @property counterClass
   * @type String
   * The CSS class to apply to the counter
   */
  counterClass: 'x-counter',
  
  /**
   * Updates the displayed number of characters remaining
   * @param {Ext.form.TextArea} textarea The textarea object
   * 
   */
  updateCount: function(textarea) {
    var length       = this.textarea.getValue().length,
        remaining    = this.maxLength - length,
        warningLimit = this.maxLength - this.warningCount,
        
        //adds the warning CSS if we're in the warning zone, removes if not
        method       = length >= warningLimit ? 'addClass' : 'removeClass';
    
    this.counter[method](this.warningClass);
    this.counter.setText(remaining);
    
    this.submitButton.setDisabled(remaining > 139 || remaining < 0);
  },
  
  onCreate: function() {
    var form = this.getForm();
    
    // Check if the form is valid
    if (form.isValid()) {
      this.fireEvent('create', form.getValues());
      this.doLayout();
    };
  },
  
  /**
   * Builds the counter 'button'
   * @return {Ext.Button} The Counter button
   */
  buildCounter: function() {
    return new Ext.Button({
      text:     this.maxLength,
      cls:      this.counterClass,
      disabled: true
    });
  },
  
  /**
   * Set up the component, create references to textarea and counter
   */
  constructor: function(config) {
    var config = config || {};
    
    /**
     * @property textarea
     * @type Ext.form.TextArea
     * The textarea used to contain microblog messages
     */
    this.textarea = new Ext.form.TextArea({
      name:       "body",
      fieldLabel: "Message",
      anchor:     "-15",
      
			grow:            true,
      enableKeyEvents: true,
      
      listeners: {
        scope:   this,
        keyup:   this.updateCount,
        'keydown': {
          fn: function(field, e) {
            this.updateCount(field);
            
            var key = Ext.EventObject.getKey();
            if (key === 13) {
              e.stopPropagation();
							if (Ext.EventObject.ctrlKey) this.onCreate();
            }
          },
          scope: this
        }
      }
    });
    
    this.counter      = this.buildCounter();
    this.submitButton = this.buildSubmitButton({
			text: Ext.ux.Translator.translate('post')
		});

    Ext.applyIf(config, {
      buttons:     [],
      items:       [this.textarea],
      bbar:        [this.counter, '->', this.submitButton],
      buttonAlign: 'left'
    });

    Kohive.views.microblogs.New.superclass.constructor.call(this, Kohive.models.MicroBlog, config);
    
    // this.initListeners();
  },
  
  /**
   * Listens to the microblogs controller and resets the textarea on update
   */
  initListeners: function() {
    Kohive.OS.getController('microblogs').on({
      scope : this,
      create: function() {
        this.textarea.setValue('');
        this.counter.setText(this.maxLength);
      }
    });
  }
});

/**
 * @class Kohive.views.overview.Index
 * @extends Kohive.Window
 * The main overview application window
 */
Kohive.views.overview.Index = Ext.extend(Kohive.Window, {
  title:      Ext.ux.Translator.translate('app_overview'),
  cls:        'x-overview-index',
  url:        'overview',
  layout:     'border',
  width:      700,
  height:     400,
  defaults:   {border: false},
	toggledAll: false,
 
  /**
   * Creates the sub-view elements
   */
  initComponent: function() {
    this.dataview = new Ext.DataView({
			id:           'overview-dataview-' + Kohive.OS.getCurrentHiveId(),
			store:        this.store,
			itemSelector: 'div.x-overview',
			
			autoHeight: true,
			
			tpl: new Ext.XTemplate(
				'<tpl for=".">',
					'<div class="x-overview {[values.reference_type.toLowerCase()]} {[values.event_type.toLowerCase()]}<tpl if="extra_new"> {extra_new}</tpl>">',
						'<div class="x-icon"><img src="{icon}" /></div>',
						'<tpl if="this.isState(event_type)"><div class="x-state-icon"></div></tpl>',
						'<div class="x-title">{translation}</div>',
						'<div class="x-date">{created_at_date}</div>',
					'</div>',
					{
						isState: function(value) {
							return value == "update_status";
						}
					},
				'</tpl>'
			),
			
			listeners: {
				'click': {
					scope: this,
					fn:    this.delegateRowClick
				}
			}
		});
		
    Ext.apply(this, {
      hbar:              false,
      bbar:              true,
      filterPanelConfig: false,
      id:                'overview_index_' + Kohive.OS.getCurrentHiveId(),
			layout:            'fit',
			
			// This bad boy fixes horizontal scrolling issues in Safari only.
			autoScroll: false,
			
      hbar: false,
			
      items: [
				{
					autoScroll: true,
					border: true,
					items:  [this.dataview]
				}
      ]
    });
    
    Kohive.views.overview.Index.superclass.initComponent.apply(this, arguments);
		
		this.initListeners();
  },
	
	initListeners: function() {
		this.on('show', function() {
			this.store.load();
			this.showLoading(Ext.ux.Translator.translate('app_statusbar_loading'));
			this.initStartPolling();
		}, this);
		
		this.on('hide', function() { if (this.runner) this.runner.stopAll(); }, this);
	},
	
	pollingStarted: false,
	
	initStartPolling: function(idCard) {
		if (this.pollingStarted) return;
		
		this.pollingStarted = true;
		
		var task = {
      run: function() {
        this.store.load();
				this.showRefreshing();
      },
      scope:    this,
      interval: 15000 // 15 seconds
    };
    this.runner = new Ext.util.TaskRunner();
    this.runner.start(task);
	},
	
	/**
	 * Method which is called when a row on the dataview is clicked. It will redirect the user to the specified
	 * item in the overview app, which means it'll open an application
	 */
	delegateRowClick: function(dataview, index, node, e) {
		var target = Ext.get(e.getTarget()),
		    el     = Ext.get(node);
		    record = dataview.getRecord(node);
		
		if (target.hasClass('author_nickname')) return this.onUserClick(record);
		else if (el.hasClass('membership')) return this.onUserClick(record);
		else return this.onAppClick(record);
	},
	
	/**
	 * Method called when a person clicks on a user, opens the users id card
	 */
	onUserClick: function(record) {
		var controller = Kohive.OS.getController('idcards');
		
		//show the loading statusbar
		this.showLoading(Ext.ux.Translator.translate('app_statusbar_loading'));
		
		Kohive.models.IdCard.findById(record.get('author_id'), {
      scope:   this,
			url:     Kohive.OS.scopedUrl(String.format("id_cards/{0}", record.get('author_id'))),
      success: function(idCard) {
				this.showSuccess();
        controller.edit(idCard);
      },
			failure: function() {
				this.showFailure();
			}
    });
	},
	
	/**
	 * Methed called when an application is clicked, opens application
	 */
	onAppClick: function(record) {
		var name = record.get('reference_type').toLowerCase().pluralize(),
		    match = Kohive.OS.router.recognise((name == "comments") ? "photos" : name);
		
		//if the match exists, dispatch pls
		if (match) Kohive.OS.dispatch(match);
	}
});

Ext.reg('kohive_app_overview_view_index', Kohive.views.overview.Index);

/**
 * Kohive.views.settings.Index
 * @extends Kohive.Window
 * Container for all settings options for Kohive
 */
Kohive.views.settings.Index = Ext.extend(Kohive.Window, {
  
  constructor: function(config) {
    var config = config || {};
		
		var view = this;
		
    this.mainPanel = new Ext.Panel({
      layout:     'card',
      region:     'center',
      cls:        'x-content-wrap',
      autoScroll: true,
      // autoHeight: true,
      // autoScroll:   true,
      defaults: {
        border:    false,
        header:    false
      }
    });
		
		this.menuPanel = new Kohive.views.settings.Menu({
      cls:    'x-menu',
      border: false,
      listeners: {
 				scope             : this,
        'menunodeclicked' : this.changeCard,
				'menuswitch'      : this.selectDefaultCard
      }
    });
		
		var views = [], ns = Kohive.views.settings.cards;
    for (var v in ns) {
			this.mainPanel.add(new ns[v]());
		};

    Ext.applyIf(config, {
			title:      Ext.ux.Translator.translate('app_settings') + ' : ' + Ext.ux.Translator.translate('personal'),
		  id:         'settings-index' + Kohive.OS.getCurrentHiveId(),
		  cls:        'x-settings-index',
		  url:        'settings',
		  layout:     'border',
		  width:      750,
		  height:     450,
		  defaults:   {border: false},
			toggledAll: false,
      border:     false,
      hbar:       false,
      // bbar:       false,

			filterPanelConfig: false,

      items: [
        {
          cls:         'x-menu-wrap',
          region:      'west',
          width:       120,
          collapsible: false,
          border:      false,
        
          items: [
            this.menuPanel
          ]
        },
        this.mainPanel
      ]
    });

    Kohive.views.settings.Index.superclass.constructor.call(this, config);

    this.initListeners();
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event beforechangecard
       * Fires before the panel in this.mainPanel is changed.  Return false from
       * any listener to cancel
       * @param {Kohive.Window} this The Settings Index view
       * @param {String} oldId The ID of the card about to be switched out
       * @param {String} id The ID of the card about to be loaded
       */
      'beforechangecard',

      /**
       * @event changecard
       * Fires after the panel in this.mainPanel has been changed
       * @param {Kohive.Window} this The Settings Index view
       * @param {String} oldId The ID of the card which has just been switched out
       * @param {String} id The ID of the card which has just been shown
       */
      'changecard'
    );
    
    Kohive.views.settings.Index.superclass.initEvents.apply(this, arguments);
  },
  
  /**
   * Set up listeners to Settings controller and Id Cards controller
   */
  initListeners: function() {
    Kohive.OS.getController('idcards').on({
      scope                 : this,
      'update'              : this.refreshIdCards,
			'hive-id-card-update' : this.onChangeCardSuccess
    });
		
    this.mainPanel.items.get('settings_view_hive_id_card' + Kohive.OS.getCurrentHiveId()).on({
      scope       : this,
      // 'refreshed' : this.hideLoading,
			'use'       : this.onChangeCard
    });
  },
  
  /**
   * Refreshes the Id Cards card if it is currently displayed
   */
  refreshIdCards: function() {
    var active = this.mainPanel.layout.activeItem;
		if (!active) return;
		
    if (active.id == "settings_view_hive_id_card" + Kohive.OS.getCurrentHiveId()) active.refresh();
  },
  
  /**
   * Changes the view ins the mainPanel to the one specified by the ID parameter.
   * Fires beforechangecard before, and changecard afterwards.  Has no effect if
   * trying to move to the same card as is currently selected.
   * @param {String} id The ID of the card to load
   */
  changeCard: function(id) {
    var oldId = this.mainPanel.layout.activeItem ? this.mainPanel.layout.activeItem.id : null;
    if (id != oldId) {
      if (this.fireEvent('beforechangecard', this, oldId, id)) {
        this.mainPanel.getLayout().setActiveItem(id);
				
        this.fireEvent('changecard', this, oldId, id);
      }
    };
  },
	
	/**
	 * Adds a updating message when the user tries to change their id card
	 * @param {Kohive.models.IdCard} idCard The Id Card now being used
	 */
	onChangeCard: function(idCard) {
		// this.showSuccess(Ext.ux.Translator.translate('app_statusbar_changed_idcard'));
		this.showUpdating();
	},
	
	/**
	 * Adds a success message to the index window when the user successfully changes
	 * their default id card for a hive.
	 * @param {Kohive.models.IdCard} idCard The Id Card now being used
	 */
	onChangeCardSuccess: function(idCard) {
		// this.showSuccess(Ext.ux.Translator.translate('app_statusbar_changed_idcard'));
		this.showSuccess();
	},
	
	/**
	 * Adds a failure message to the index window when something goes wrong when the
	 * user tries to change their id card
	 * @param {Kohive.models.IdCard} idCard The Id Card now being used
	 */
	onChangeCardFailure: function(idCard) {
		// this.showSuccess(Ext.ux.Translator.translate('app_statusbar_changed_idcard'));
		this.showFailure();
	},
	
	/**
	 * Method which shows the default card for each type (personal/hive)
	 */
	selectDefaultCard: function(menu, oldType, newType) {
		if (newType == "personal") this.selectDefaultPersonalCard();
		else this.selectDefaultHiveCard();
	},
	
	/**
	 * Method which selects the default personal tab view
	 */
	selectDefaultPersonalCard: function() {
	  var id = "settings_view_personal_account" + Kohive.OS.getCurrentHiveId();

		this.mainPanel.getLayout().setActiveItem(id);
    this.fireEvent('changecard', this, null, id);
	},
	
	/**
	 * Method which selects the default personal tab view
	 */
	selectDefaultHiveCard: function() {
	  var id = "settings_view_hive_id_card" + Kohive.OS.getCurrentHiveId();

		this.mainPanel.getLayout().setActiveItem(id);
    this.fireEvent('changecard', this, null, id);
	}
});

Ext.reg('kohive_app_settings_view_index', Kohive.views.settings.Index); 

/**
 * Kohive.views.settings.Menu
 * @extends Ext.tree.TreePanel
 * Manages menu items on the Kohive Settings application
 */
Kohive.views.settings.Menu = function(config) {
  var config = config || {};
  
  this.personalBtn = new Ext.Button({
    template: new Ext.Template(
      '<div>',
        '<div class="button" />',
      '</div>'
    ),
    onRender: function(ct, position){
      btn = this.template.append(ct, {}, true);
      this.el = btn;
      
      var btnEl = btn.child('div.button');

      this.initButtonEl(btn, btnEl);
      
      Ext.ButtonToggleMgr.register(this);
    },
    cls:          'personal',
    allowDepress: false,
    toggleGroup:  'settings_toolbar',
    // Id is needed for the defaultButton
    id:           'settings_toolbar_button_personal' + Kohive.OS.getCurrentHiveId(),
    handler:      this.switchToType.createDelegate(this, ['personal'])
  });
  
  this.hiveBtn = new Ext.Button({
    template: new Ext.Template(
      '<div>',
        '<div class="button" />',
      '</div>'
    ),
    onRender: function(ct, position){
      btn = this.template.append(ct, {}, true);
      this.el = btn;
      
      var btnEl = btn.child('div.button');

      this.initButtonEl(btn, btnEl);
      
      Ext.ButtonToggleMgr.register(this);
    },
    cls:          'hive',
    allowDepress: false,
    toggleGroup:  'settings_toolbar',
    // Id is needed for the defaultButton
    id:           'settings_toolbar_button_hive' + Kohive.OS.getCurrentHiveId(),
    handler:      this.switchToType.createDelegate(this, ['hive'])
  });
	
  Ext.applyIf(config, {
    id:          'settings_menu' + Kohive.OS.getCurrentHiveId(),
    cls:         'x-settings-menu',
    root:        new Ext.tree.TreeNode('Settings Menu'),
    rootVisible: false,
    lines:       false,
    autoScroll:  true,
    enableDD:    true,
    stateful:    true,
    stateEvents: ['movenode'],
    border:      false,
    
    defaultMenuType: 'hive'
    
			//     tbar: new Ext.Toolbar({
			// id: 'settings_menu_toolbar' + Kohive.OS.getCurrentHiveId(),
			//       defaults: {
			//         enableToggle: true
			//       },
			//       items: [personalBtn, hiveBtn]
			//     })
  });
  
  /**
   * @property menuTypes
   * @type Array
   * Array of menu types registered.  Would usually just contain hive and personal
   */
  this.menuTypes = [];
  
  /**
   * @property menuItems
   * @type Object
   * Holds references to all registered menu items, keyed by menu type
   */
  this.menuItems = {};
  
  /**
   * @property currentMenuType
   * @type String
   * Records the type of the current menu being displayed (personal or hive)
   */
  this.currentMenuType = '';
 
  Kohive.views.settings.Menu.superclass.constructor.call(this, config);
  
  this.addEvents(
    /**
     * @event menunodeclicked
     * Fires whenever a menu item is clicked
     * @param {String} id The unique ID of the menu node which was clicked.  This should
     * correspond to the ID of a panel in the Settings app's SlidingCardLayout
     */
    'menunodeclicked',
    
    /**
     * @event beforemenuswitch
     * Fires before the menu switches (e.g. from hive to personal).  Return false from any listener to cancel switch.
     * @param {Ext.tree.TreePanel} this The menu tree panel
     * @param {String} oldMenuType The string name of the menu which is currently displayed
     * @param {String} newMenuType The string name of the menu about to be shown (e.g. 'hive' or 'personal')
     */
    'beforemenuswitch',
    
    /**
     * @event menuswitch
     * Fires after the menu has been switched
     * @param {Ext.tree.TreePanel} this The menu tree panel
     * @param {String} oldMenuType The string name of the previously selected menu
     * @param {String} newMenuType The string name of the newly selected menu
     */
    'menuswitch'
  );
  
  //FIXME: this doesn't feel right :(
  this.on('render', function() {
		this.initToolbar();
		
    var defaultButton = Ext.getCmp('settings_toolbar_button_' + (this.initialConfig.defaultMenuType || this.menuTypes[0]) + Kohive.OS.getCurrentHiveId());
    // var defaultButton = Ext.getCmp('settings_toolbar_button_hive' + Kohive.OS.getCurrentHiveId());
    
		//simulate a click on the default menu button
    if (defaultButton) {
      defaultButton.handler();
      defaultButton.toggle();
    };
  }, this);
};

Ext.extend(Kohive.views.settings.Menu, Ext.tree.TreePanel, {
  
	/**
	 * sets up the toolbar inside the window
	 */
	initToolbar: function() {
		var win = Ext.get('settings-index' + Kohive.OS.getCurrentHiveId());
		if (!win) return;
		
		var toolbar = new Ext.Toolbar({
			id: 'settings_menu_toolbar_' + Kohive.OS.getCurrentHiveId(),
			renderTo: win,
	    defaults: {
	      enableToggle: true
	    },
	    items: [this.personalBtn, this.hiveBtn]
	  });
	},
	
  /**
   * Sets the component up with Personal and Hive types, switches to the default menu.  
   */
  initComponent: function() {
    this.registerMenuTypes('hive', 'personal');
    
    var iconPath = "/images/apps/Settings/icons/";
    
    this.registerMenuItem('hive', {text: Ext.ux.Translator.translate('id_card'),       id: 'settings_view_hive_id_card' + Kohive.OS.getCurrentHiveId(),       icon: iconPath + 'idcard.png'});
    this.registerMenuItem('hive', {text: Ext.ux.Translator.translate('notifications'), id: 'settings_view_hive_notifications' + Kohive.OS.getCurrentHiveId(), icon: iconPath + 'notifications.png'});
    if (Kohive.OS.hasPermission(true)) { this.registerMenuItem('hive', {text: Ext.ux.Translator.translate('info'),          id: 'settings_view_hive_info' + Kohive.OS.getCurrentHiveId(),          icon: iconPath + 'accountinfo.png'}); };
    // this.registerMenuItem('hive',     {text: Ext.ux.Translator.translate('quick_apps'),    id: 'settings_view_hive_quick_apps' + Kohive.OS.getCurrentHiveId(),    icon: iconPath + 'quickapps.png'});
    // this.registerMenuItem('hive',     {text: Ext.ux.Translator.translate('widgets'),       id: 'settings_view_hive_widgets' + Kohive.OS.getCurrentHiveId(),           icon: iconPath + 'widgets.png'});
    if (Kohive.OS.hasPermission(true)) { this.registerMenuItem('hive', {text: Ext.ux.Translator.translate('applications'), id: 'settings_view_hive_applications' + Kohive.OS.getCurrentHiveId(), icon: iconPath + 'applications.png'}); };
    if (Kohive.OS.hasPermission(true)) { this.registerMenuItem('hive', {text: Ext.ux.Translator.translate('appearance'),   id: 'settings_view_hive_appearance' + Kohive.OS.getCurrentHiveId(),   icon: iconPath + 'appearance.png'}); };
    
    
    this.registerMenuItem('personal', {text: Ext.ux.Translator.translate('account_info'),  id: 'settings_view_personal_account' + Kohive.OS.getCurrentHiveId(), icon: iconPath + 'accountinfo.png'});
    this.registerMenuItem('personal', {text: Ext.ux.Translator.translate('notifications'), id: 'settings_view_personal_notifications' + Kohive.OS.getCurrentHiveId(), icon: iconPath + 'notifications.png'});
    this.registerMenuItem('personal', {text: Ext.ux.Translator.translate('language'),      id: 'settings_view_personal_language' + Kohive.OS.getCurrentHiveId(), icon: iconPath + 'language.png'});
    // this.registerMenuItem('personal', {text: 'Widgets',       id: 'settings_view_personal_widgets' + Kohive.OS.getCurrentHiveId(),       icon: iconPath + 'widgets.png'});
    
    Kohive.views.settings.Menu.superclass.initComponent.apply(this, arguments);
  },
  
  /**
   * Removes all nodes from the tree
   */
  clearItems: function() {
    for (var i = this.root.childNodes.length - 1; i >= 0; i--){
      this.root.removeChild(this.root.childNodes[i]);
    };
  },
  
  /**
   * Registers a string key to allow creation of a set of menu items keyed to a type.
   * This is then used with this.switchToType to switch one menu for another 
   * @param {String} type The name of the menu type to register 
   */
  registerMenuType: function(type) {
    this.menuTypes.push(type);
  },
  
  /**
   * Convenience function in place of calling this.registerMenuType many times 
   * Each parameter is passed in turn to this.registerMenuType
   */
  registerMenuTypes: function() {
    for (var i=0; i < arguments.length; i++) {
      this.registerMenuType(arguments[i]);
    };
  },
  
  /**
   * Registers a menu item with a certain type (hive or personal).  Items are displayed
   * on the menu depending on whether Hive or Personal is selected 
   * @param {String} type hive or personal 
   */
  registerMenuItem: function(type, itemConfig) {
    this.menuItems[type] = this.menuItems[type] || [];
    
    // Set draggable to false so the user can't drag it. Don't know why this is as docs says it is false by default
    Ext.applyIf(itemConfig, {
      draggable: false
    });
    
    this.menuItems[type].push(itemConfig);
  },
  
  /**
   * Switches the menu to the chosen type (e.g. pass personal to load the personal menu)
   * Updates this.currentMenuType to the new type
   * @param {String} menuType The type of menu to show (hive or personal) 
   */
  switchToType: function(menuType) {
    if (menuType != this.currentMenuType) {
      if (this.fireEvent('beforemenuswitch', this, this.currentMenuType, menuType)) {
        var oldMenuType = this.currentMenuType;
        
        this.clearItems();
        Ext.each((this.menuItems[menuType] || []), function(item) { this.addItem(item); }, this);
        this.currentMenuType = menuType;
        
        var controller = Kohive.OS.getController('settings');
        if (controller) {
          controller.indexWindow.setTitle(Ext.ux.Translator.translate('app_settings') + ' : ' + Ext.ux.Translator.translate(menuType));
        };
        
        this.fireEvent('menuswitch', this, oldMenuType, this.currentMenuType);
      }   
    };
  },
  
  /**
   * Adds a node to this tree, registers any required listeners
   * @param {Object} options Configuration options for the SettingsTreeNode to add
   * @return {Ext.ux.tree.SettingsTreeNode} The newly added node
   */
  addItem: function(options) {
    var options = options || {};
    Ext.applyIf(options, {
      listeners: {
        'click': {
          fn: function(node, e) {
            this.fireEvent('menunodeclicked', node.id);
          },
          scope: this
        }
      }
    });
    
    return this.root.appendChild(new Ext.ux.tree.SettingsTreeNode(options));
  },
  
	/**
	 * 
	 */
	setActiveItem: function(id) {
		//get the type and switch to it
		var type = id.split("_")[2];
		this.switchToType(type);
	},
	
  /**
   * Serialises the state of the tree into a string
   * @return {Object} Representation of this component's state
   */
  getState: function() {
    ////console.log('getting state');
    
    // boo = this;
    
    // return {menuItems: this.menuItems};
    return {};
  },
  
  /**
   * Applies a state object to this component
   * @param {Object} state State object to apply
   */
  applyState: function(state) {
    // ////console.log('applying state');
    // ////console.log(state);
  }
});

Ext.reg('settings_menu', Kohive.views.settings.Menu); 

/**
 * Kohive.views.settings.cards.HiveAppearance
 * @extends Ext.form.FormPanel
 * Allows user to update Hive appearance - theme, background image etc
 */
Kohive.views.settings.cards.HiveAppearance = function(config) {
  var config = config || {};
  
  // Get the os, controller and the main index view
  var os         = ExtMVC.OS.getOS();
  var controller = os.getController('settings');
  
  // Top bar dataview
  this.headerDataView = new Ext.DataView({
    xtype:        'dataview',
    emptyText:    Ext.ux.Translator.translate('no_items_to_display'),
    singleSelect: (Kohive.OS.hasPermission()) ? true : false,
    autoHeight:   true,
    overClass:    'x-view-over',
    itemSelector: (Kohive.OS.hasPermission()) ? 'div.thumb-wrap' : '',
    store:        Kohive.OS.getHeaderColours(),
    tpl:          new Ext.XTemplate(
      '<tpl for=".">',
        '<div class="thumb-wrap" id="header-background-colour-{colour}">',
          '<div class="thumb-inner-wrap">',
            '<div class="thumb-selected"></div>',
            '<div class="thumb headerPanel {colour}"></div>',
          '</div>',
        '</div>',
      '</tpl>'
    ),
    listeners: {
      'click': {
        scope: this,
        fn: function(dataView, index, node, e) {
          if (!Kohive.OS.hasPermission()) { return; };
          
          var colour = node.id.split("-")[node.id.split("-").length - 1];
          
          Kohive.OS.getHeaderPanel().setBackgroundColour(colour, {
            success: function() {
              controller.indexWindow.showSuccess();
            },
            failure: function() {
              controller.indexWindow.showFailure();
            }
          });
        }
      }
    }
  });
  
  if (!Kohive.OS.hasPermission()) { this.headerDataView.disable(); };
  
  // Background dataview
  this.backgroundDataView = new Ext.DataView({
    xtype:        'dataview',
    emptyText:    Ext.ux.Translator.translate('no_items_to_display'),
    singleSelect: (Kohive.OS.hasPermission()) ? true : false,
    autoHeight:   true,
    overClass:    'x-view-over',
    itemSelector: (Kohive.OS.hasPermission()) ? 'div.thumb-wrap' : '',
    store:        Kohive.OS.getBackgroundColours(),
    tpl:          new Ext.XTemplate(
      '<tpl for=".">',
        '<div class="thumb-wrap" ref="{image}">',
          '<div class="thumb-inner-wrap">',
           '<div class="thumb-selected"></div>',
           '<img src="{thumb}" />',
          '</div>',
        '</div>',
      '</tpl>'
    ),
    listeners: {
      'click': {
        scope: this,
        fn: function(dataView, index, node, e) {
          if (!Kohive.OS.hasPermission()) return;
          
          // var colour = node.getAttribute('ref');
          var colour = Kohive.OS.getBackgroundColours().data.items[index].data;
          
          Kohive.OS.setBackgroundColour(colour, {
            success: function() {
              controller.indexWindow.showSuccess();
            },
            failure: function() {
              controller.indexWindow.showFailure();
            }
          });
        }
      }
    }
  });
  
  if (!Kohive.OS.hasPermission()) { this.backgroundDataView.disable(); };
  
  Ext.applyIf(config, {
    id:         'settings_view_hive_appearance' + Kohive.OS.getCurrentHiveId(),
    cls:        'settings_view_hive_appearance',
    title:      Ext.ux.Translator.translate('hive') + ' : ' + Ext.ux.Translator.translate('appearance'),
    header:     false,
    autoScroll: true,
    
    items:  [
      {
        xtype:      'fieldset',
        title:      Ext.ux.Translator.translate('header'),
        autoHeight: true,
        items: [this.headerDataView]
      },
      {
        xtype:      'fieldset',
        title:      Ext.ux.Translator.translate('background'),
        autoHeight: true,
        items: [this.backgroundDataView]
      }
    ]
  });
 
  Kohive.views.settings.cards.HiveAppearance.superclass.constructor.call(this, config);
  
  this.on('activate', this.getSelected, this);
};
Ext.extend(Kohive.views.settings.cards.HiveAppearance, Ext.form.FormPanel, {
  /**
   * Selects the currently selected header colour or background for the hive
   */
  getSelected: function() {
    var hive = Kohive.OS.getCurrentHive().get('hive');
    
    var tempTask = new Ext.util.DelayedTask(function() {
      // Get the header selected node
      var hsn = Ext.get('header-background-colour-' + hive.top_toolbar_colour);
      if (hsn) { this.headerDataView.select(hsn.dom); } else { this.headerDataView.select(4); };
      
      // Get the background selected node
      var bn = this.backgroundDataView.el.select('.thumb-wrap');
      for (var i = 0; i < bn.elements.length; i++) {
        var el = bn.elements[i];
				
        if (el.getAttribute('ref') == hive.background_image) {
          return this.backgroundDataView.select(el);
        } else {
          this.backgroundDataView.select(0);
        };
      };
      
    }, this);
    tempTask.delay(500);
  }
});

Ext.reg('settings_view_hive_appearance', Kohive.views.settings.cards.HiveAppearance);

/**
 * Kohive.views.settings.cards.HiveApplications
 * @extends Ext.form.FormPanel
 * Allows administration of which applications are available to a hive
 */
Kohive.views.settings.cards.HiveApplications = function(config) {
  var config = config || {};
  
  // Get the os, controller and the main index view
  var os          = ExtMVC.OS.getOS();
  this.controller = os.getController('settings');
	
  /**
   * @property store
   * @type Ext.data.Store
   * The store bound to this view's DataView.  Read-only
   */
  this.standardStore = Kohive.models.Application.findAll({
    url:      '/memberships/' + os.getCurrentHive().get('id') + '/applications',
    autoLoad: false,
    listeners: {
      'load': {
        scope: this,
        fn: function(store, records) {
          store.each(function(record) {
						if (record.get('title') == "Settings" || record.get('title') == "Overview") store.remove(record);
						// if (!record.get('enabled')) store.remove(record);
					}, this);
        }
      }
    }
  });

  // /**
  //    * @property store
  //    * @type Ext.data.Store
  //    * The store bound to this view's DataView.  Read-only
  //    */
  //   this.proStore = Kohive.models.Application.findAll({
  //     url:      '/memberships/' + os.getCurrentHive().get('id') + '/applications_pro',
  //     autoLoad: false,
  //     listeners: {
  //       'load': {
  //         scope: this,
  //         fn: function(store, records) {
  //           store.each(function(record) {
  //          if (record.get('title') == "Settings" || record.get('title') == "Overview") store.remove(record);
  //          // if (!record.get('enabled')) store.remove(record);
  //        }, this);
  //         }
  //       }
  //     }
  //   });
  
	var tpl = new Ext.XTemplate(
    '<tpl for=".">',
      '<div class="x-application">',
				'<div class="x-icon"><img src="/images/components/ApplicationLauncher/icons/{[values.title.camelize().toLowerCase()]}.png" /></div>',
				'<div class="x-title">',
					'{[Ext.ux.Translator.translate("app_" + values.title.camelize().toLowerCase())]}',
				'</div>',
        '<div class="x-button">',
					'<div class="kohive-icon-button green x-kohive-icon-button<tpl if="!enabled"> x-item-disabled</tpl>" style="width: auto;"><button class="switch<tpl if="!enabled"> x-disabled</tpl><tpl if="!hidden && enabled"> on</tpl>"></button><tpl if="!enabled"><span class="x-coming-soon">' + Ext.ux.Translator.translate('coming_soon') + '</span></tpl></div>',
				'</div>',
      '</div>',
    '</tpl>'
 	);
	
  /**
   * @property datastore
   * @type Ext.DataView
   * Custom dataview for displaying the ID Cards.
   */
  this.standardDataView = new Ext.DataView({
    autoHeight:   true,
    overClass:    'x-view-over',
    border:       'false',
    itemSelector: 'div.x-application',
    store:        this.standardStore,
    tpl:          tpl,
    listeners: {
			'click': {
				scope: this,
				fn: this.delegateClick
			},
      'beforeload': {
        scope: this,
        fn: function() {
          controller.indexWindow.showLoading();
        }
      },
      'load': {
        scope: this,
        fn: function(store, records) {
          controller.indexWindow.hideLoading();
        }
      },
      'loadexception': {
        scope: this,
        fn: function() {
          controller.indexWindow.showFailure();
        }
      }
    }
  });

	/**
   * @property datastore
   * @type Ext.DataView
   * Custom dataview for displaying the ID Cards.
   */
  this.proDataView = new Ext.DataView({
    autoHeight:   true,
    overClass:    'x-view-over',
    border:       'false',
    itemSelector: 'div.x-application',
    store:        this.proStore,
    tpl:          tpl,
		listeners: {
			'click': {
				scope: this,
				fn: this.delegateClick
			}
    }
	});
  
  Ext.applyIf(config, {
		id:         'settings_view_hive_applications' + Kohive.OS.getCurrentHiveId(),
    cls:        'settings_view_hive_applications',
    title:      Ext.ux.Translator.translate('hive') + ' : ' + Ext.ux.Translator.translate('applications'),
    header:     false,
    autoScroll: true,
    
		items: [
      // {
      //  id:         'pro_apps' + Kohive.OS.getCurrentHiveId(),
      //  xtype:      'fieldset',
      //  title:      Ext.ux.Translator.translate('pro_applications'),
      //   autoHeight: true,
      //  defaults: {
      //    border: false
      //  },
      //  items: [this.proDataView]
      // },
			{
				xtype:      'fieldset',
        // title:      false,
        // header:     false,
        // title:      Ext.ux.Translator.translate('applications'),
			  autoHeight: true,
				defaults: {
					border: false
				},
				items: [this.standardDataView]
			}
		]
  });
 
  Kohive.views.settings.cards.HiveApplications.superclass.constructor.call(this, config);
	
	this.initListeners();
};
Ext.extend(Kohive.views.settings.cards.HiveApplications, Ext.form.FormPanel, {
	initListeners: function() {
		this.on('show', function() {
			this.standardStore.load();
			
      // if (Kohive.OS.getCurrentHive().get('hive').level > 0) {
      //         // this.proStore.load();
      // } else {
      //  Ext.get('pro_apps' + Kohive.OS.getCurrentHiveId()).enableDisplayMode();
      //  Ext.get('pro_apps' + Kohive.OS.getCurrentHiveId()).hide();
      // };
		}, this);
	},
	
	delegateClick: function(dataview, index, node, e) {
		var el     = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
		
		//check if it is the switch that has been clicked or it is disabled
		if (!el.hasClass('switch')) return;
		if (el.hasClass('x-disabled')) return;
		
		//update the record
		record.set('hidden', (record.get('hidden')) ? false : true);
		
		//save the record
		record.save({
			url: this.controller.scopedUrl(String.format("application_hives/{0}", record.get('application_hives_id'))),
			success: function() {
				Kohive.OS.getApplicationLauncher().populateFromHive();
			},
			failure: function() {
				record.set('hidden', (record.get('hidden')) ? false : true);
			}
		});
	}
});

Ext.reg('settings_view_hive_applications', Kohive.views.settings.cards.HiveApplications);

/**
 * @class Kohive.views.settings.cards.HiveIdCard
 * @extends Ext.Panel
 * Allows the user to choose an existing ID Card to use for this Hive, or create a new one
 */
Kohive.views.settings.cards.HiveIdCard = Ext.extend(Ext.Panel, {

  initComponent: function() {
    /**
     * @property dataview
     * @type Ext.DataView
     * The dataview used to render the Id Cards
     */
    this.dataview = new Ext.DataView({
      emptyText:    '',
      singleSelect: true,
      overClass:    'x-view-over',
      border:       false,
      itemSelector: 'div.idcard-wrap',
      tpl:          new Ext.XTemplate(
        '<tpl for=".">',
          '<div id="id_card-{id}-{[Kohive.OS.getCurrentHiveId()]}" class="idcard-wrap{[xcount == 1 ? " single" : ""]}{[Kohive.OS.getCurrentUser().get("default_id_card_id") == values.id ? " single" : ""]}">',
            '<div class="option-use"></div>',
            '<div class="option-delete"></div>',
            '<div class="main">',
              '<div class="top option-show">',
                '<div class="image option-show">',
                  '<img src="{medium_filename}" align="left" />',
                '</div>',
                '<h3 class="option-show">{name}</h3>',
                '<div class="nickname">{nickname}</div>',
                '<div class="full_name">{full_name}</div>',
                '<tpl if="job_title"><div class="job_title">{job_title}</div></tpl>',
              '</div>',
              '<div class="botton option-show">',
								'<ul>',
                '<tpl if="email"><li><span>&#8226;</span> <tpl if="!can_edit"><a href="mailto:{email}"></tpl>{email}<tpl if="can_edit"></a></tpl><div class="verified<tpl if="verified_email"> true</tpl><tpl if="!verified_email"> false</tpl>"></div></li></tpl>',
                '<tpl if="phone"><li><span>&#8226;</span> {phone}</li></tpl>',
                '<tpl if="website"><li><span>&#8226;</span> <tpl if="!can_edit"><a href="{website}"></tpl>{website}<tpl if="!can_edit"></a></tpl></li></tpl>',
								'<ul>',
              '</div>',
            '</div>',
          '</div>',
        '</tpl>'
      )
    });
    
    Ext.apply(this, {
      id:         'settings_view_hive_id_card' + Kohive.OS.getCurrentHiveId(),
      cls:        'settings_view_hive_id_card',
      plugins:    [new Ext.ux.HeaderToolbar()],
      title:      Kohive.OS.getCurrentHive().get('hive').title + ' : ' + Ext.ux.Translator.translate('app_id_cards'),
      header:     false,
      autoScroll: true,
      items:   [
        {
          xtype:  'panel',
          border: false,
          items:  [this.dataview]
        }
      ],
      hbar: [
        {
          text:     Ext.ux.Translator.translate('nnew'),
          iconCls:  'new',
          xtype:    'header_toolbar_button',
          scope:    this,
          handler:  function() {
						if (this.dataview.store.totalLength >= Kohive.OS.getCurrentUser().maxIdCards()) return Ext.Msg.alert('', Ext.ux.Translator.translate('id_card_pro_hive_error'));
						this.fireEvent('add');
					}
        }
      ]
    });
    
    Kohive.views.settings.cards.HiveIdCard.superclass.initComponent.apply(this, arguments);
    
    this.initEvents();
    this.initListeners();
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event refresh
       * Fires when the user clicks the refresh button
       */
      'refresh',
      
      /**
       * @event refreshed
       * Fires after the store bound to the dataview has been refreshed
       */
      'refreshed',
      
      /**
       * @event add
       * Fires when the user clicks the 'Add' Id Card button
       */
      'add',
      
      /**
       * @event edit
       * Fires when the user wants to edit an Id Card
       * @param {Kohive.models.IdCard} idCard The Id Card the user wants to edit
       */
      'edit',
      
      /**
       * @event destroy
       * Fires when the user wants to destroy an Id Card
       * @param {Kohive.models.IdCard} idCard The Id Card the user wants to destroy
       */
      'destroy',
			
			/**
			 * @event verify
			 * Fires when the user wants to verify the id card email
       * @param {Kohive.models.IdCard} idCard The Id Card the user wants to verify
			 */
			'verify',
      
      /**
       * @event use
       * Fires when the user clicks the 'use' button on an Id Card
       * @param {Kohive.models.IdCard} idCard The Id Card the user wants to use
       */
      'use'
    );
  },
  
  /**
   * Sets up listeners on the Id Cards controller
   */
  initListeners: function() {
    Kohive.OS.getController('idcards').on({
      scope                : this,
      'hive-id-card-update': this.refresh,
      'destroy'            : this.refresh,
      'create'             : this.refresh
    });
    
    this.on({
      scope     : this,
      'show'    : this.onActivate,
      'refresh' : this.refresh,
			'hide'    : function() { if (this.runner) this.runner.stopAll(); }
    });
		
    this.dataview.on({
      scope: this,
      click: function(dataview, index, node, e) {
        var idCard = dataview.getRecord(node),
            el     = Ext.get(e.getTarget());
        
        if      (el.hasClass('option-use'))    this.fireEvent('use', idCard);
        else if (el.hasClass('option-delete')) this.onDestroyClick(idCard);
        else if (el.hasClass('false'))         this.onVerifyClick(idCard);
        else    this.fireEvent('edit', idCard);
      }
    });
  },
  
  /**
   * Called when the user clicks the button to destroy a particular Id Card
   * @param {Kohive.models.IdCard} idCard The Id Card the user wishes to destroy
   */
  onDestroyClick: function(idCard) {
    return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('destroy_id_card_desc', { title: idCard.get('name')}),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
          this.fireEvent('destroy', idCard);
        }
      }   
    });
  },
	
	onVerifyClick: function(idCard) {
    return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('verify_id_card_email'),
      buttons: {no:Ext.ux.Translator.translate('verify_again'), yes:Ext.ux.Translator.translate('cancel')},
      fn:      function(msg) {
        if (msg == 'no') {
          this.fireEvent('verify', idCard);
					this.initStartPolling();
        }
      }   
    });
  },
  
	initStartPolling: function(idCard) {
		if (this.pollingStarted) return;
		if (!Kohive.OS.loopingMode) return;
		
		this.pollingStarted = true;
		
		var task = {
      run: function() {
        Kohive.models.IdCard.findById(idCard.get('id'), {
		      scope: this,
					url:   Kohive.OS.getController('idcards').scopedUrl(String.format("id_cards/{0}", idCard.get('id'))),
		      success: function(idCard) {
		        this.updateShowView(idCard);
						this.pollingStarted = false;
		      }
		    });
      },
      scope:    this,
      interval: 8000 // 8 seconds
    };
    this.runner = new Ext.util.TaskRunner();
    this.runner.start(task);
	},
	
	updateShowView: function(idCard) {
		if (idCard.get('verified_email') === true) {
			Kohive.OS.getController('idcards').fireEvent('hive-id-card-update', this);
			var record = this.dataview.store.getById(idCard.get('id'));
			if (record) record.set('verified_email', true);
			else this.refresh();
		};
	},
	
  /**
   * Refreshes the underlying DataView
   */
  refresh: function() {
		if (!this.dataview || !this.dataview.store) return;
		
    this.dataview.store.reload({
      scope:    this, 
      callback: function() {
        this.fireEvent('refreshed');
      }
    });
  },
  
  /**
   * Highlights the current Id Card by adding a CSS class to it
   */
  highlightCurrentIdCard: function() {
    var mem = Kohive.OS.getCurrentHive(); //returns a membership instead
    
    this.dataview.store.each(function(idCard) {
      if (idCard.get('id') == mem.get('id_card_id')) {
        Ext.get('id_card-' + idCard.get('id') + '-' + Kohive.OS.getCurrentHiveId()).addClass('selected');
      }
    }, this);
  },
	
	checkForVerified: function() {
		this.dataview.store.each(function(idCard) {
      if (!idCard.get('verified_email')) this.initStartPolling(idCard);
    }, this);
	},
	
  /**
   * Fired when this panel is first activated.
   * Fires the id cards controller's index action with a reference to this view
   */
  onActivate: function() {
		var tempTask = new Ext.util.DelayedTask(function() {
      var store = Kohive.OS.getController('idcards').index(this);
	    store.on('load', this.highlightCurrentIdCard, this);
	    store.on('load', this.checkForVerified, this);

	    this.dataview.setStore(store);
    }, this); 
    
    tempTask.delay(500);
  }
});

Ext.reg('settings_view_hive_id_card', Kohive.views.settings.cards.HiveIdCard);

/**
 * Kohive.views.settings.cards.HiveInfo
 * @extends Ext.form.FormPanel
 * Allows user to update Hive information
 */
Kohive.views.settings.cards.HiveInfo = function(config) {
  var config = config || {};
  
  // Get the os, controller and the main index view
  var os         = ExtMVC.OS.getOS();
  var controller = os.getController('settings');
  
  /**
	 * @property detailsForm
	 * @type Ext.form.FormPanel
	 */
	this.detailsForm = new Ext.form.FormPanel({
		header:      false,
		border:      false,
		autoHeight:  true,
    labelWidth:  130,
    labelAlign:  'right',
		buttonAlign: 'right',
		style:       'margin-top:10px',
    
    defaults: {
      xtype: 'textfield',
			width: 160
    },
    
		keys: [
      {
        key :      Ext.EventObject.ENTER,
        scope:     this,
        handler:   function(key, e) {
          var target = e.getTarget();
          if (target.type == "text") {
            this.onChangeDetails();
          };
        }
      }
    ],
		
    items: [
     	{
        fieldLabel: Ext.ux.Translator.translate('name'),
        id:         'hive_info_name' + Kohive.OS.getCurrentHiveId(),
        name:       'title',
        allowBlank: false,
        width:      90
      },
      {
        fieldLabel:  Ext.ux.Translator.translate('invite_url'),
        id:         'hive_info_invite_hashcode' + Kohive.OS.getCurrentHiveId(),
        name:       'invite_hashcode',
        allowBlank: false,
        width:      70
      }
    ],
		
		buttons: [
			new Kohive.SmallButton({
        text:     Ext.ux.Translator.translate('save'),
        scope:    this,
        handler:  this.onChangeDetails
      })
		]
	});
  
  Ext.applyIf(config, {
    id:         'settings_view_hive_info' + Kohive.OS.getCurrentHiveId(),
    cls:        'settings_view_hive_info',
    title:      Ext.ux.Translator.translate('hive') + ' : ' + Ext.ux.Translator.translate('info'),
    header:     false,
    autoScroll: true,
    
    items: [this.detailsForm]
  });
 
  Kohive.views.settings.cards.HiveInfo.superclass.constructor.call(this, config);
  
  this.initListeners();
};
Ext.extend(Kohive.views.settings.cards.HiveInfo, Ext.Panel, {
  /**
   * 
   */
  initListeners: function() {
    this.on('render', this.addDefaultValues, this);
    this.on('show',   this.addFieldLabel, this);
  },
  
  /**
   * 
   */
  addDefaultValues: function() {
    if (Kohive.OS.getCurrentHive() && Kohive.OS.hasPermission()) {
      var name            = Kohive.OS.getCurrentHive().get('hive').title;
      var invite_hashcode = Kohive.OS.getCurrentHive().get('hive').invite_hashcode;
      
      Ext.getCmp('hive_info_name' + Kohive.OS.getCurrentHiveId()).setValue(name);
      Ext.getCmp('hive_info_invite_hashcode' + Kohive.OS.getCurrentHiveId()).setValue(invite_hashcode);
    } else {
      Ext.getCmp('hive_info_name' + Kohive.OS.getCurrentHiveId()).disable();
      Ext.getCmp('hive_info_invite_hashcode' + Kohive.OS.getCurrentHiveId()).disable();
    };
  },
  
  addFieldLabel: function() {
    if (this.fieldAdded) return;
    this.fieldAdded = true;
    
    var field = Ext.getCmp('hive_info_invite_hashcode' + Kohive.OS.getCurrentHiveId());
    if (field.el) {
      var newEl = field.el.insertSibling({
        cls:  'x-invite-hash-text',
        html: 'http://www.kohive.com/'
      });
    };
  },
  
  /**
   * 
   */
  onChangeDetails: function() {
    if (!this.detailsForm.form.isValid()) return;
    
    var controller = Kohive.OS.getController('settings');
    if (controller && controller.indexWindow) controller.indexWindow.showLoading(Ext.ux.Translator.translate('app_statusbar_loading'));
    
    var name            = Ext.getCmp('hive_info_name' + Kohive.OS.getCurrentHiveId()).getValue() || "";
    var invite_hashcode = Ext.getCmp('hive_info_invite_hashcode' + Kohive.OS.getCurrentHiveId()).getValue() || "";
    
    var currentHive = Kohive.OS.getCurrentHive().get('hive');
    var hive = new Kohive.models.Hive({
      id:              currentHive.id,
      level:           currentHive.level,
      title:           name,
      invite_hashcode: invite_hashcode
    });
    
    hive.save({
      success: function() {
        if (controller && controller.indexWindow) controller.indexWindow.showSuccess();
        Kohive.OS.getController('hives').fireAction('refresh', Kohive.OS.getController('hives'));
      },
      failure: function() {
        if (controller && controller.indexWindow) controller.indexWindow.showFailure();
      }
    });
  }
});

Ext.reg('settings_view_hive_info', Kohive.views.settings.cards.HiveInfo);

Kohive.NotificationButton = Ext.extend(Kohive.IconButton, {
	iconCls:    'switch',
	
	onRender: function(ct, position) {
    Kohive.NotificationButton.superclass.onRender.call(this, ct, position);

		Kohive.OS.getCurrentMembership().subscribedToEmail(this.eventClass) ?
		  this.el.child('button').addClass('on') :
		  this.el.child('button').removeClass('on');
  }
});


/**
 * Kohive.views.settings.cards.HiveNotifications
 * @extends Ext.form.FormPanel
 * Settings config options for Hive Notification settings
 */
Kohive.views.settings.cards.HiveNotifications = function(config) {
  var config = config || {},
      type   = Kohive.OS.getCurrentHive().get('hive').type;
	
	Ext.applyIf(config, {
    id:         'settings_view_hive_notifications' + Kohive.OS.getCurrentHiveId(),
    cls:        'settings_view_hive_appearance',
    title:      Ext.ux.Translator.translate('hive') + ' : ' + Ext.ux.Translator.translate('notifications'),
    header:     false,
    autoScroll: true,
    
    items:  [
      {
        xtype:      'fieldset',
        title:      Ext.ux.Translator.translate('email'),
        autoHeight: true,
				defaults: {
					border: false
				},
        items: [
					{
						layout: 'column',
						height: 20,
						hidden: (type == "standard") ? true : false,
						defaults: {
							border: false
						},
						items: [
							{
								columnWidth: .6,
								width:       220,
								html:        Ext.ux.Translator.translate('notifications_task_assigned_to_me')
							},
							{
								columnWidth: .4,
								width:       22,
								items: [
									new Kohive.NotificationButton({
								    scope:      this,
								    handler:    this.onUpdate,
                    eventClass: "TaskAssignedToYouEvent"
								  })
								]
							}
						]
					},
					{
						layout: 'column',
						height: 20,
						hidden: (type == "standard") ? true : false,
						defaults: {
							border: false
						},
						items: [
							{
								columnWidth: .6,
								width:       220,
								html:        Ext.ux.Translator.translate('notifications_task_of_you_completed')
							},
							{
								columnWidth: .4,
								width:       22,
								items: [
									new Kohive.NotificationButton({
								    scope:      this,
								    handler:    this.onUpdate,
                    eventClass: "TaskOfYouCompletedEvent"
								  })
								]
							}
						]
					},
					{
						layout: 'column',
						height: 20,
						defaults: {
							border: false
						},
						items: [
							{
								columnWidth: .6,
								width:       220,
								html:        Ext.ux.Translator.translate('notifications_message_board_mention')
							},
							{
								columnWidth: .4,
								width:       22,
								items: [
								new Kohive.NotificationButton({
							    scope:      this,
							    handler:    this.onUpdate,
                  eventClass: "BoardPostReceivedEvent"
							  })
								]
							}
						]
					},
					{
						layout: 'column',
						height: 20,
						defaults: {
							border: false
						},
						items: [
							{
								columnWidth: .6,
								width:       220,
								html:        (type == "standard") ? Ext.ux.Translator.translate('notifications_new_comment') : Ext.ux.Translator.translate('notifications_new_comment_task')
							},
							{
								columnWidth: .4,
								width:       22,
								items: [
								new Kohive.NotificationButton({
							    scope:      this,
							    handler:    this.onUpdate,
                  eventClass: "CommentOnItemOfYouEvent,CommentOnItemYouCommentedEvent,CommentOnItemAssignedToYouEvent"
							  })
								]
							}
						]
					}
					// {
					// 	layout: 'column',	
					//  height: 20,
					// 	defaults: {
					// 		border: false
					// 	},
					// 	items: [
					// 		{
					// 			columnWidth: .4,
					// 			width:       22,
					// 			items: [
					// 				new Kohive.IconButton({
					// 			    iconCls:  'switch',
					// 			    scope:    this,
					// 			    handler:  this.onUpdate,
					// 					onRender: function(ct, position) {
					// 			      Kohive.IconButton.superclass.onRender.call(this, ct, position);
					// 
					// 						var membership = Kohive.OS.getCurrentHive();
					// 						if (membership.get('email_notifications') === "true") this.el.child('button').addClass('on');
					// 						else this.el.child('button').removeClass('on');
					// 			    }
					// 			  })
					// 			]
					// 		},
					// 		{
					// 			columnWidth: .6,
					// 			html:        Ext.ux.Translator.translate('notifications_new_comment_blog')
					// 		}
					// 	]
					// },
          // {
          //  layout: 'column',
          //  height: 20,
          //  defaults: {
          //    border: false
          //  },
          //  items: [
          //    {
          //      columnWidth: .6,
          //      width:       220,
          //      html:        Ext.ux.Translator.translate('notifications_new_hive_member')
          //    },
          //    {
          //      columnWidth: .4,
          //      width:       22,
          //      items: [
          //        new Kohive.IconButton({
          //          iconCls:  'switch',
          //          scope:    this,
          //          disabled: true,
          //          handler:  this.onUpdate,
          //          onRender: function(ct, position) {
          //            Kohive.IconButton.superclass.onRender.call(this, ct, position);
          // 
          //            var membership = Kohive.OS.getCurrentHive();
          //            if (membership.get('email_notifications') === "true") this.el.child('button').addClass('on');
          //            else this.el.child('button').removeClass('on');
          //          }
          //        })
          //      ]
          //    }
          //  ]
          // }
				]
      }
    ]
  });
 
	Kohive.views.settings.cards.HiveNotifications.superclass.constructor.call(this, config);
  
	this.addEvents(
	  /**
	   * @event update
	   * Fires when the notifications form is updated
	   * @param {Ext.form.FormPanel} formPanel The form panel instance
	   * @param {Ext.form.BasicForm} form The form being submitted
	   */
	  'update'
	);
};
Ext.extend(Kohive.views.settings.cards.HiveNotifications, Ext.Panel, {
	/**
	 * Called when the email notification button is clicked
	 */
	onUpdate: function(btn) {
		// Check if the button is enabled or not
		var btnEl   = btn.el.child('button'),
		    wasEnabled = btnEl.hasClass('on');
		
		var membership = Kohive.OS.getCurrentMembership();
		
		this.showUpdating();
		
		if (wasEnabled) {
			membership.unsubscribeToEmail(btn.eventClass);
			membership.save({
				scope:   this,
				success: function() {
					btnEl.removeClass('on');
					this.showSuccess();
				}
			});
		} else {
			membership.subscribeToEmail(btn.eventClass);
			membership.save({
				scope:   this,
				success: function() {
					btnEl.addClass('on');
					this.showSuccess();
				}
			});
		};
	},
	
	showUpdating: function() {
		var v = Ext.getCmp('settings-index'+Kohive.OS.getCurrentHiveId());
		v.showUpdating();
	},
	
	showSuccess: function() {
		var v = Ext.getCmp('settings-index'+Kohive.OS.getCurrentHiveId());
		v.showSuccess();
	}
});

Ext.reg('settings_view_hive_notifications', Kohive.views.settings.cards.HiveNotifications);

/**
 * Kohive.views.settings.cards.HiveQuickApps
 * @extends Ext.form.FormPanel
 * Allows user to choose which quick apps are available on this hive
 */
Kohive.views.settings.cards.HiveQuickApps = function(config) {
  var config = config || {};
 
  Ext.applyIf(config, {
    id:     'settings_view_hive_quick_apps' + Kohive.OS.getCurrentHiveId(),
    title:  'Hive : Quick Apps',
    header: false,
    
    items: [
      {
        xtype:      'textfield',
        name:       'somename',
        fieldLabel: 'Gorramit!'
      }
    ]
  });
 
  Kohive.views.settings.cards.HiveQuickApps.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.views.settings.cards.HiveQuickApps, Ext.form.FormPanel);

Ext.reg('settings_view_hive_quick_apps', Kohive.views.settings.cards.HiveQuickApps);

/**
 * Kohive.views.settings.cards.HiveWidgets
 * @extends Ext.form.FormPanel
 * Allows additional and removal of widgets from a Hive
 */
Kohive.views.settings.cards.HiveWidgets = function(config) {
  var config = config || {};
 
  Ext.applyIf(config, {
    id:     'settings_view_hive_widgets' + Kohive.OS.getCurrentHiveId(),
    title:  'Hive : Widgets',
    header: false,
    
    items: [
      {
        xtype:      'textfield',
        fieldLabel: 'This is not here',
        name:       'frell',
        value:      'Seriously'
      }
    ]
  });
 
  Kohive.views.settings.cards.HiveWidgets.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.views.settings.cards.HiveWidgets, Ext.form.FormPanel);

Ext.reg('settings_view_hive_widgets', Kohive.views.settings.cards.HiveWidgets);

Ext.data.States = [
	['', '', ''],
	['AL', 'Alabama', 'The Heart of Dixie'],
	['AK', 'Alaska', 'The Land of the Midnight Sun'],
	['AZ', 'Arizona', 'The Grand Canyon State'],
	['AR', 'Arkansas', 'The Natural State'],
	['CA', 'California', 'The Golden State'],
	['CO', 'Colorado', 'The Mountain State'],
	['CT', 'Connecticut', 'The Constitution State'],
	['DE', 'Delaware', 'The First State'],
	['DC', 'District of Columbia', "The Nation's Capital"],
	['FL', 'Florida', 'The Sunshine State'],
	['GA', 'Georgia', 'The Peach State'],
	['HI', 'Hawaii', 'The Aloha State'],
	['ID', 'Idaho', 'Famous Potatoes'],
	['IL', 'Illinois', 'The Prairie State'],
	['IN', 'Indiana', 'The Hospitality State'],
	['IA', 'Iowa', 'The Corn State'],
	['KS', 'Kansas', 'The Sunflower State'],
	['KY', 'Kentucky', 'The Bluegrass State'],
	['LA', 'Louisiana', 'The Bayou State'],
	['ME', 'Maine', 'The Pine Tree State'],
	['MD', 'Maryland', 'Chesapeake State'],
	['MA', 'Massachusetts', 'The Spirit of America'],
	['MI', 'Michigan', 'Great Lakes State'],
	['MN', 'Minnesota', 'North Star State'],
	['MS', 'Mississippi', 'Magnolia State'],
	['MO', 'Missouri', 'Show Me State'],
	['MT', 'Montana', 'Big Sky Country'],
	['NE', 'Nebraska', 'Beef State'],
	['NV', 'Nevada', 'Silver State'],
	['NH', 'New Hampshire', 'Granite State'],
	['NJ', 'New Jersey', 'Garden State'],
	['NM', 'New Mexico', 'Land of Enchantment'],
	['NY', 'New York', 'Empire State'],
	['NC', 'North Carolina', 'First in Freedom'],
	['ND', 'North Dakota', 'Peace Garden State'],
	['OH', 'Ohio', 'The Heart of it All'],
	['OK', 'Oklahoma', 'Oklahoma is OK'],
	['OR', 'Oregon', 'Pacific Wonderland'],
	['PA', 'Pennsylvania', 'Keystone State'],
	['RI', 'Rhode Island', 'Ocean State'],
	['SC', 'South Carolina', 'Nothing Could be Finer'],
	['SD', 'South Dakota', 'Great Faces, Great Places'],
	['TN', 'Tennessee', 'Volunteer State'],
	['TX', 'Texas', 'Lone Star State'],
	['UT', 'Utah', 'Salt Lake State'],
	['VT', 'Vermont', 'Green Mountain State'],
	['VA', 'Virginia', 'Mother of States'],
	['WA', 'Washington', 'Green Tree State'],
	['WV', 'West Virginia', 'Mountain State'],
	['WI', 'Wisconsin', "America's Dairyland"],
	['WY', 'Wyoming', 'Like No Place on Earth']
];

Ext.data.Countries = [
	['GB', 'United Kingdom'],
	['US', 'United States'],
	
	['', '-----------------'],
	['AF', 'Afghanistan'],
	['AL', 'Albania'],
	['DZ', 'Algeria'],
	['AS', 'American Samoa'],
	['AD', 'Andorra'],
	['AG', 'Angola'],
	['AI', 'Anguilla'],
	['AG', 'Antigua &amp; Barbuda'],
	['AR', 'Argentina'],
	['AA', 'Armenia'],
	['AW', 'Aruba'],
	['AU', 'Australia'],
	['AT', 'Austria'],
	['AZ', 'Azerbaijan'],
	['BS', 'Bahamas'],
	['BH', 'Bahrain'],
	['BD', 'Bangladesh'],
	['BB', 'Barbados'],
	['BY', 'Belarus'],
	['BE', 'Belgium'],
	['BZ', 'Belize'],
	['BJ', 'Benin'],
	['BM', 'Bermuda'],
	['BT', 'Bhutan'],
	['BO', 'Bolivia'],
	['BL', 'Bonaire'],
	['BA', 'Bosnia &amp; Herzegovina'],
	['BW', 'Botswana'],
	['BR', 'Brazil'],
	['BC', 'British Indian Ocean Ter'],
	['BN', 'Brunei'],
	['BG', 'Bulgaria'],
	['BF', 'Burkina Faso'],
	['BI', 'Burundi'],
	['KH', 'Cambodia'],
	['CM', 'Cameroon'],
	['CA', 'Canada'],
	['IC', 'Canary Islands'],
	['CV', 'Cape Verde'],
	['KY', 'Cayman Islands'],
	['CF', 'Central African Republic'],
	['TD', 'Chad'],
	['CD', 'Channel Islands'],
	['CL', 'Chile'],
	['CN', 'China'],
	['CI', 'Christmas Island'],
	['CS', 'Cocos Island'],
	['CO', 'Colombia'],
	['CC', 'Comoros'],
	['CG', 'Congo'],
	['CK', 'Cook Islands'],
	['CR', 'Costa Rica'],
	['CT', 'Cote D\'Ivoire'],
	['HR', 'Croatia'],
	['CU', 'Cuba'],
	['CB', 'Curacao'],
	['CY', 'Cyprus'],
	['CZ', 'Czech Republic'],
	['DK', 'Denmark'],
	['DJ', 'Djibouti'],
	['DM', 'Dominica'],
	['DO', 'Dominican Republic'],
	['TM', 'East Timor'],
	['EC', 'Ecuador'],
	['EG', 'Egypt'],
	['SV', 'El Salvador'],
	['GQ', 'Equatorial Guinea'],
	['ER', 'Eritrea'],
	['EE', 'Estonia'],
	['ET', 'Ethiopia'],
	['FA', 'Falkland Islands'],
	['FO', 'Faroe Islands'],
	['FJ', 'Fiji'],
	['FI', 'Finland'],
	['FR', 'France'],
	['GF', 'French Guiana'],
	['PF', 'French Polynesia'],
	['FS', 'French Southern Ter'],
	['GA', 'Gabon'],
	['GM', 'Gambia'],
	['GE', 'Georgia'],
	['DE', 'Germany'],
	['GH', 'Ghana'],
	['GI', 'Gibraltar'],
	['GB', 'Great Britain'],
	['GR', 'Greece'],
	['GL', 'Greenland'],
	['GD', 'Grenada'],
	['GP', 'Guadeloupe'],
	['GU', 'Guam'],
	['GT', 'Guatemala'],
	['GN', 'Guinea'],
	['GY', 'Guyana'],
	['HT', 'Haiti'],
	['HW', 'Hawaii'],
	['HN', 'Honduras'],
	['HK', 'Hong Kong'],
	['HU', 'Hungary'],
	['IS', 'Iceland'],
	['IN', 'India'],
	['ID', 'Indonesia'],
	['IA', 'Iran'],
	['IQ', 'Iraq'],
	['IR', 'Ireland'],
	['IM', 'Isle of Man'],
	['IL', 'Israel'],
	['IT', 'Italy'],
	['JM', 'Jamaica'],
	['JP', 'Japan'],
	['JO', 'Jordan'],
	['KZ', 'Kazakhstan'],
	['KE', 'Kenya'],
	['KI', 'Kiribati'],
	['NK', 'Korea North'],
	['KS', 'Korea South'],
	['KW', 'Kuwait'],
	['KG', 'Kyrgyzstan'],
	['LA', 'Laos'],
	['LV', 'Latvia'],
	['LB', 'Lebanon'],
	['LS', 'Lesotho'],
	['LR', 'Liberia'],
	['LY', 'Libya'],
	['LI', 'Liechtenstein'],
	['LT', 'Lithuania'],
	['LU', 'Luxembourg'],
	['MO', 'Macau'],
	['MK', 'Macedonia'],
	['MG', 'Madagascar'],
	['MY', 'Malaysia'],
	['MW', 'Malawi'],
	['MV', 'Maldives'],
	['ML', 'Mali'],
	['MT', 'Malta'],
	['MH', 'Marshall Islands'],
	['MQ', 'Martinique'],
	['MR', 'Mauritania'],
	['MU', 'Mauritius'],
	['ME', 'Mayotte'],
	['MX', 'Mexico'],
	['MI', 'Midway Islands'],
	['MD', 'Moldova'],
	['MC', 'Monaco'],
	['MN', 'Mongolia'],
	['MS', 'Montserrat'],
	['MA', 'Morocco'],
	['MZ', 'Mozambique'],
	['MM', 'Myanmar'],
	['NA', 'Nambia'],
	['NU', 'Nauru'],
	['NP', 'Nepal'],
	['AN', 'Netherland Antilles'],
	['NL', 'Netherlands (Holland, Europe)'],
	['NV', 'Nevis'],
	['NC', 'New Caledonia'],
	['NZ', 'New Zealand'],
	['NI', 'Nicaragua'],
	['NE', 'Niger'],
	['NG', 'Nigeria'],
	['NW', 'Niue'],
	['NF', 'Norfolk Island'],
	['NO', 'Norway'],
	['OM', 'Oman'],
	['PK', 'Pakistan'],
	['PW', 'Palau Island'],
	['PS', 'Palestine'],
	['PA', 'Panama'],
	['PG', 'Papua New Guinea'],
	['PY', 'Paraguay'],
	['PE', 'Peru'],
	['PH', 'Philippines'],
	['PO', 'Pitcairn Island'],
	['PL', 'Poland'],
	['PT', 'Portugal'],
	['PR', 'Puerto Rico'],
	['QA', 'Qatar'],
	['ME', 'Republic of Montenegro'],
	['RS', 'Republic of Serbia'],
	['RE', 'Reunion'],
	['RO', 'Romania'],
	['RU', 'Russia'],
	['RW', 'Rwanda'],
	['NT', 'St Barthelemy'],
	['EU', 'St Eustatius'],
	['HE', 'St Helena'],
	['KN', 'St Kitts-Nevis'],
	['LC', 'St Lucia'],
	['MB', 'St Maarten'],
	['PM', 'St Pierre &amp; Miquelon'],
	['VC', 'St Vincent &amp; Grenadines'],
	['SP', 'Saipan'],
	['SO', 'Samoa'],
	['AS', 'Samoa American'],
	['SM', 'San Marino'],
	['ST', 'Sao Tome &amp; Principe'],
	['SA', 'Saudi Arabia'],
	['SN', 'Senegal'],
	['SC', 'Seychelles'],
	['SL', 'Sierra Leone'],
	['SG', 'Singapore'],
	['SK', 'Slovakia'],
	['SI', 'Slovenia'],
	['SB', 'Solomon Islands'],
	['OI', 'Somalia'],
	['ZA', 'South Africa'],
	['ES', 'Spain'],
	['LK', 'Sri Lanka'],
	['SD', 'Sudan'],
	['SR', 'Suriname'],
	['SZ', 'Swaziland'],
	['SE', 'Sweden'],
	['CH', 'Switzerland'],
	['SY', 'Syria'],
	['TA', 'Tahiti'],
	['TW', 'Taiwan'],
	['TJ', 'Tajikistan'],
	['TZ', 'Tanzania'],
	['TH', 'Thailand'],
	['TG', 'Togo'],
	['TK', 'Tokelau'],
	['TO', 'Tonga'],
	['TT', 'Trinidad &amp; Tobago'],
	['TN', 'Tunisia'],
	['TR', 'Turkey'],
	['TU', 'Turkmenistan'],
	['TC', 'Turks &amp; Caicos Is'],
	['TV', 'Tuvalu'],
	['UG', 'Uganda'],
	['UA', 'Ukraine'],
	['AE', 'United Arab Emirates'],
	['GB', 'United Kingdom'],
	['US', 'United States of America'],
	['UY', 'Uruguay'],
	['UZ', 'Uzbekistan'],
	['VU', 'Vanuatu'],
	['VS', 'Vatican City State'],
	['VE', 'Venezuela'],
	['VN', 'Vietnam'],
	['VB', 'Virgin Islands (Brit)'],
	['VA', 'Virgin Islands (USA)'],
	['WK', 'Wake Island'],
	['WF', 'Wallis &amp; Futana Is'],
	['YE', 'Yemen'],
	['ZR', 'Zaire'],
	['ZM', 'Zambia'],
	['ZW', 'Zimbabwe']
];

/**
 * Kohive.views.settings.cards.PersonalAccount
 * @extends Ext.form.FormPanel
 * Settings config options for Personal Account settings
 */
Kohive.views.settings.cards.PersonalAccount = function(config) {
  var config = config || {};
	
	/**
	 * @property changePricePlanPanel
	 * @type Ext.Panel
	 * The panel which will include a grid to change your price plan
	 */
	this.changePricePlanPanel = new Ext.Panel({
		html: new Ext.XTemplate(
			'<div class="x-plans-wrapper">',
        // '<div class="x-table-plans-error">',
        //   '<div class="x-message">testing</div>',
        // '</div>',
  			'<div class="x-table-plans-wrapper">',
  				'<table cellpadding="0" cellspacing="0" class="x-table-plans paid">',
  					'<tr class="top">',
  						'<td valign="middle">Free<br /><span class="info">' + Ext.ux.Translator.translate('plans_free_info') + '</span></td>',
  						'<td valign="middle" colspan="2" class="free-col checkbox" id="switch-free' + Kohive.OS.getCurrentHiveId() + '"></td>',
  					'</tr>',
  					'<tr class="header">',
  						'<td valign="middle" class="colleft">Premium</td>',
  						'<td valign="middle" width="100" class="col">Monthly</td>',
  						'<td valign="middle" width="100">Annual</td>',
  					'</tr>',
  					'<tr>',
  						'<td valign="middle">Bronze<br /><span class="info">' + Ext.ux.Translator.translate('plans_bronze_info') + '</span></td>',
  						'<td valign="middle" class="checkbox col" id="switch-bronze-monthly' + Kohive.OS.getCurrentHiveId() + '"></td>',
  						'<td valign="middle" class="checkbox" id="switch-bronze-annually' + Kohive.OS.getCurrentHiveId() + '"></td>',
  					'</tr>',
  					'<tr>',
  						'<td valign="middle">Silver<br /><span class="info">' + Ext.ux.Translator.translate('plans_silver_info') + '</span></td>',
  						'<td valign="middle" class="checkbox col" id="switch-silver-monthly' + Kohive.OS.getCurrentHiveId() + '"></td>',
  						'<td valign="middle" class="checkbox" id="switch-silver-annually' + Kohive.OS.getCurrentHiveId() + '"></td>',
  					'</tr>',
  					'<tr>',
  						'<td valign="middle">Gold<br /><span class="info">' + Ext.ux.Translator.translate('plans_gold_info') + '</span></td>',
  						'<td valign="middle" class="checkbox col" id="switch-gold-monthly' + Kohive.OS.getCurrentHiveId() + '"></td>',
  						'<td valign="middle" class="checkbox" id="switch-gold-annually' + Kohive.OS.getCurrentHiveId() + '"></td>',
  					'</tr>',
  				'</table>',
  			'</div>',
  			'<div class="bottom">',
				  '<div class="currencies">',
				    '<div class="pound active">&pound;</div>',
				    '<div class="dollar">$</div>',
				    '<div class="euro">&euro;</div>',
  				'</div>',
  				'<div class="renewal-date">' + Ext.ux.Translator.translate('next_billing_date') + ': ' + Kohive.OS.getCurrentUser().get('renewal_date') + '</div>',
				'</div>',
			'</div>'
		)
	});
	
	this.creditCardMonths = [];
	for (var m = 1; m <= 12; m++)
	  this.creditCardMonths.push([m, m]);
	
	this.creditCardYears = [];
	for (var y = (new Date()).format('Y'); y <= (new Date()).format('Y') + 10; y++)
	  this.creditCardYears.push([y, y]);
	
	/**
	 * @property billingInformationFormPanel
	 * @type Ext.form.FormPanel
	 * The form to input your biiliing information
	 */
	this.billingInformationFormPanel = new Ext.form.FormPanel({
		header:      false,
		autoHeight:  true,
    labelWidth:  130,
    labelAlign:  'right',
		buttonAlign: 'right',
    
    defaults: {
      xtype: 'textfield',
			width: 160
    },
		
		keys: [
      {
        key :      Ext.EventObject.ENTER,
        scope:     this,
        handler:   function(key, e) {
          var target = e.getTarget();
          if (target.type == "text") {
            this.onUpdate();
          };
        }
      },
      {
        key:       's',
        ctrl:      true,
        scope:     this,
        stopEvent: true,
        handler:   this.onBillingInformationUpdate
      }
    ],
		
		items: [
     	{
        id:        'personal_account_selected_plan_type' + Kohive.OS.getCurrentHiveId(),
        name:      'selected_plan_type',
        xtype:     'hidden'
      },
     	{
        id:        'personal_account_selected_billing_period' + Kohive.OS.getCurrentHiveId(),
        name:      'selected_billing_period',
        xtype:     'hidden'
      },
      
			{
        fieldLabel: Ext.ux.Translator.translate('first_name'),
        id:        'personal_account_first_name' + Kohive.OS.getCurrentHiveId(),
        name:      'credit_card[first_name]'
      },
			{
        fieldLabel: Ext.ux.Translator.translate('last_name'),
        id:        'personal_account_last_name' + Kohive.OS.getCurrentHiveId(),
        name:      'credit_card[last_name]'
      },
     	{
        fieldLabel: Ext.ux.Translator.translate('credit_card_number'),
        id:        'personal_account_credit_card_number' + Kohive.OS.getCurrentHiveId(),
        name:      'credit_card[number]' //,
        // vtype:     'creditcard'
      },
			{
        fieldLabel: Ext.ux.Translator.translate('credit_card_type'),
        id:        'personal_account_credit_card_type' + Kohive.OS.getCurrentHiveId(),
        hiddenName:      'credit_card[type]',
				xtype:     'combo',
				width:     160,
				store: [["visa", "Visa"], ["master", "MasterCard"], ["american_express", "American Express"]],
        emptyText: 'Select a type...',
        typeAhead: true,
        forceSelection: true,
        triggerAction: 'all',
        selectOnFocus: true
      },
			{
        fieldLabel: Ext.ux.Translator.translate('credit_card_month'),
        id:        'personal_account_credit_card_month' + Kohive.OS.getCurrentHiveId(),
        name:      'credit_card[month]',
				xtype:     'combo',
				width:     100,
				store:     this.creditCardMonths,
        typeAhead: true,
        forceSelection: true,
        triggerAction: 'all',
        selectOnFocus: true
      },
			{
        fieldLabel: Ext.ux.Translator.translate('credit_card_year'),
        id:        'personal_account_credit_card_year' + Kohive.OS.getCurrentHiveId(),
        name:      'credit_card[year]',
				xtype:     'combo',
				width:     100,
				store:     this.creditCardYears,
        typeAhead: true,
        forceSelection: true,
        triggerAction: 'all',
        selectOnFocus: true
      },
      {
        fieldLabel: Ext.ux.Translator.translate('security_code'),
        id:        'personal_account_verification_value' + Kohive.OS.getCurrentHiveId(),
        name:      'credit_card[verification_value]',
        width:     30
      },
			{
        fieldLabel: Ext.ux.Translator.translate('address'),
        id:        'personal_account_address1' + Kohive.OS.getCurrentHiveId(),
        name:      'address[address1]'
      },
			{
				labelSeparator: '',
        id:        'personal_account_address2' + Kohive.OS.getCurrentHiveId(),
        name:      'address[address2]'
      },
			{
        fieldLabel: Ext.ux.Translator.translate('state'),
        id:        'personal_account_state' + Kohive.OS.getCurrentHiveId(),
        name:      'address[state]'
      },
			{
        fieldLabel: Ext.ux.Translator.translate('post_code'),
        id:        'personal_account_post_code' + Kohive.OS.getCurrentHiveId(),
        name:      'address[zip]',
				width:     60
      },
			{
        fieldLabel: Ext.ux.Translator.translate('country'),
        id:         'personal_account_country' + Kohive.OS.getCurrentHiveId(),
        hiddenName: 'address[country]',
				xtype:      'combo',
				
				width: 160,
				
				store:     new Ext.data.SimpleStore({
	        fields: ['abbr', 'country'],
	        data:   Ext.data.Countries
		    }),
		    
		    listeners: {
		      'show' : function() {
		        var wrapper = this.el.parent(),
		            width   = this.initialConfig.width || null;
		        
		        if (wrapper && width) wrapper.setWidth(width);
		      }
		    },
				
				displayField:   'country',
				valueField:     'abbr',
        typeAhead:      true,
        mode:           'local',
        forceSelection: true,
        triggerAction:  'all',
        emptyText:      'Select a country...',
        selectOnFocus:  true
      },
  		{
        fieldLabel: Ext.ux.Translator.translate('discount_code'),
        id:        'personal_account_discount_code' + Kohive.OS.getCurrentHiveId(),
        name:      'discount_code',
  			width:     100
      }
    ],
		
		buttons: [
			new Kohive.SmallButton({
        text:     Ext.ux.Translator.translate('pay'),
        scope:    this,
        handler:  this.onBillingInformationUpdate
      })
		]
	});
	
	/**
	 * @property changePasswordFormPanel
	 * @type Ext.form.FormPanel
	 * The form for changing your account password
	 */
	this.changePasswordFormPanel = new Ext.form.FormPanel({
		header:      false,
		autoHeight:  true,
    labelWidth:  130,
    labelAlign:  'right',
		buttonAlign: 'right',
    
    defaults: {
      xtype: 'textfield',
			width: 160
    },
    
		keys: [
      {
        key :      Ext.EventObject.ENTER,
        scope:     this,
        handler:   function(key, e) {
          var target = e.getTarget();
          if (target.type == "text") {
            this.onUpdate();
          };
        }
      },
      {
        key:       's',
        ctrl:      true,
        scope:     this,
        stopEvent: true,
        handler:   this.onChangePasswordUpdate
      }
    ],
		
    items: [
				//       {
				//         fieldLabel: Ext.ux.Translator.translate('email'),
				//         name:       'email',
				//         allowBlank: false,
				//         vtype:      'email',
				// value:      Kohive.OS.getCurrentUser().get('email'),
				// id:        'personal_account_email' + Kohive.OS.getCurrentHiveId()
				//       },
     	{
        fieldLabel: Ext.ux.Translator.translate('old_password'),
        id:        'personal_account_old_password' + Kohive.OS.getCurrentHiveId(),
        name:      'old_password',
        inputType: 'password'
      },
			{
        fieldLabel: Ext.ux.Translator.translate('new_password'),
        id:        'personal_account_password' + Kohive.OS.getCurrentHiveId(),
        name:      'password',
        inputType: 'password',
        vtype:     'password',
				minLength: 6
      },
      {
        fieldLabel: Ext.ux.Translator.translate('confirm_new_password'),
        id:        'personal_account_confirm_password' + Kohive.OS.getCurrentHiveId(),
        name:      'password_confirmation',
        inputType: 'password',
        vtype:     'password',
				minLength: 6,
        matches:   'personal_account_password' + Kohive.OS.getCurrentHiveId()
      }
    ],
		
		buttons: [
			new Kohive.SmallButton({
        text:     Ext.ux.Translator.translate('change'),
        scope:    this,
        handler:  this.onChangePasswordUpdate
      })
		]
	});
	
	// this.closeAccountForm = new Ext.Panel({
	// 	style:       'margin-right:20px',
	// 	border:      false,
	// 	buttonAlign: 'right',
	// 	
	// 	buttons: [
	// 		new Kohive.Button({
	//         text:     Ext.ux.Translator.translate('close_account'),
	// 			color:    'white',
	//         scope:    this,
	//         handler:  this.onDestroy
	//       })
	// 	]
	// });
	
  Ext.applyIf(config, {
		id:         'settings_view_personal_account' + Kohive.OS.getCurrentHiveId(),
    cls:        'settings_view_hive_appearance',
    title:      Ext.ux.Translator.translate('personal') + ' : ' + Ext.ux.Translator.translate('account_info'),
    header:     false,
    autoScroll: true,
    
    items:  [
            // //change price plan
            // {
            //         xtype:      'fieldset',
            //         title:      Ext.ux.Translator.translate('price_plan'),
            //         autoHeight: true,
            //  defaults: {
            //      border: false
            //  },
            //         items: [
            //      this.changePricePlanPanel
            //  ]
            // },
            // 
            // //billing information
            // {
            //         xtype: 'fieldset',
            //         title: Ext.ux.Translator.translate('billing_information'),
            //  
            //  id: 'personal_account_billing_information' + Kohive.OS.getCurrentHiveId(),
            //  
            //         autoHeight: true,
            //  defaults: {
            //      border: false
            //  },
            //         items: [
            //      this.billingInformationFormPanel
            //  ]
            // },
			
			//change password form
      {
        xtype:      'fieldset',
        title:      Ext.ux.Translator.translate('change_password'),
        autoHeight: true,
				defaults: {
					border: false
				},
        items: [
					this.changePasswordFormPanel
				]
			}
		]
  });
 
	Kohive.views.settings.cards.PersonalAccount.superclass.constructor.call(this, config);
  
	this.addEvents(
	  /**
	   * @event update
	   * Fires when the account form is updated
	   * @param {Ext.form.FormPanel} formPanel The form panel instance
	   * @param {Ext.form.BasicForm} form The form being submitted
	   */
	  'update'
	);
	
	this.initListeners();
};
Ext.extend(Kohive.views.settings.cards.PersonalAccount, Ext.Panel, {
	/**
	 * 
	 */
	initListeners: function() {
		this.on('show', function() {
			this.doLayout();
			this.initBillingInformation();
			this.initButtons();
		}, this);
	},
	
	pricePlanChanged: false,
	
	initButtons: function() {
	  if (!Ext.get('switch-free' + Kohive.OS.getCurrentHiveId()).child('button')) {
  		//add switches
  		this.freeSwitch           = new Kohive.IconButton({iconCls:'radio free',renderTo:'switch-free' + Kohive.OS.getCurrentHiveId()});
  		this.bronzeMonthlySwitch  = new Kohive.IconButton({iconCls:'radio bronze',renderTo:'switch-bronze-monthly' + Kohive.OS.getCurrentHiveId()});
  		this.bronzeAnnuallySwitch = new Kohive.IconButton({iconCls:'radio bronze',renderTo:'switch-bronze-annually' + Kohive.OS.getCurrentHiveId()});
  		this.silverMonthlySwitch  = new Kohive.IconButton({iconCls:'radio silver',renderTo:'switch-silver-monthly' + Kohive.OS.getCurrentHiveId()});
  		this.silverAnnuallySwitch = new Kohive.IconButton({iconCls:'radio silver',renderTo:'switch-silver-annually' + Kohive.OS.getCurrentHiveId()});
  		this.goldMonthlySwitch    = new Kohive.IconButton({iconCls:'radio gold',renderTo:'switch-gold-monthly' + Kohive.OS.getCurrentHiveId()});
  		this.goldAnnuallySwitch   = new Kohive.IconButton({iconCls:'radio gold',renderTo:'switch-gold-annually' + Kohive.OS.getCurrentHiveId()});
		
  		// Select the current price plan
  		switch (Kohive.OS.getCurrentUser().data.plan_switch_code) {
  		  case 'free': this.freeSwitch.el.child('button').addClass('on'); break;
  		  case 'bronze-monthly': this.bronzeMonthlySwitch.el.child('button').addClass('on'); break;
  		  case 'bronze-annually': this.bronzeAnnuallySwitch.el.child('button').addClass('on'); break;
  		  case 'bronze-trial-monthly': this.bronzeMonthlySwitch.el.child('button').addClass('on'); break;
  		  case 'bronze-trial-annually': this.bronzeAnnuallySwitch.el.child('button').addClass('on'); break;
  		  case 'silver-monthly': this.silverMonthlySwitch.el.child('button').addClass('on'); break;
  		  case 'silver-annually': this.silverAnnuallySwitch.el.child('button').addClass('on'); break;
  		  case 'gold-monthly': this.goldMonthlySwitch.el.child('button').addClass('on'); break;
  		  case 'gold-monthly': this.goldAnnuallySwitch.el.child('button').addClass('on'); break;
  		}
		
  		//add prices
  		this.bronzeMonthlyPrice  = Ext.get('switch-bronze-monthly' + Kohive.OS.getCurrentHiveId()).createChild({tag:'span',cls:'price',html: '&pound;10'});
  		this.bronzeAnnuallyPrice = Ext.get('switch-bronze-annually' + Kohive.OS.getCurrentHiveId()).createChild({tag:'span',cls:'price',html: '&pound;50'});
  		this.silverMonthlyPrice  = Ext.get('switch-silver-monthly' + Kohive.OS.getCurrentHiveId()).createChild({tag:'span',cls:'price',html: '&pound;20'});
  		this.silverAnnuallyPrice = Ext.get('switch-silver-annually' + Kohive.OS.getCurrentHiveId()).createChild({tag:'span',cls:'price',html: '&pound;100'});
  		this.goldMonthlyPrice    = Ext.get('switch-gold-monthly' + Kohive.OS.getCurrentHiveId()).createChild({tag:'span',cls:'price',html: '&pound;50'});
  		this.goldAnnuallyPrice   = Ext.get('switch-gold-annually' + Kohive.OS.getCurrentHiveId()).createChild({tag:'span',cls:'price',html: '&pound;250'});
  		
  		this.initButtonListeners();
  		
  		this.initCurrency();
  	};
	},
	
	/**
	 * 
	 */
	initButtonListeners: function() {
		this.freeSwitch.on('click',           this.onButtonClick.createDelegate(this, ['free']), this);
		this.bronzeMonthlySwitch.on('click',  this.onButtonClick.createDelegate(this, ['bronze', 'monthly']), this);
		this.bronzeAnnuallySwitch.on('click', this.onButtonClick.createDelegate(this, ['bronze', 'annually']), this);
		this.silverMonthlySwitch.on('click',  this.onButtonClick.createDelegate(this, ['silver', 'monthly']), this);
		this.silverAnnuallySwitch.on('click', this.onButtonClick.createDelegate(this, ['silver', 'annually']), this);
		this.goldMonthlySwitch.on('click',    this.onButtonClick.createDelegate(this, ['gold', 'monthly']), this);
		this.goldAnnuallySwitch.on('click',   this.onButtonClick.createDelegate(this, ['gold', 'annually']), this);
	},
	
	/**
	 * 
	 */
	onButtonClick: function(plan_type, billing_period) {
		if (!plan_type) return;
		var billing_period = billing_period || null;
		
		Ext.get('switch-free' + Kohive.OS.getCurrentHiveId()).child('button').removeClass('on');
		Ext.get('switch-bronze-monthly' + Kohive.OS.getCurrentHiveId()).child('button').removeClass('on');
		Ext.get('switch-bronze-annually' + Kohive.OS.getCurrentHiveId()).child('button').removeClass('on');
		Ext.get('switch-silver-monthly' + Kohive.OS.getCurrentHiveId()).child('button').removeClass('on');
		Ext.get('switch-silver-annually' + Kohive.OS.getCurrentHiveId()).child('button').removeClass('on');
		Ext.get('switch-gold-monthly' + Kohive.OS.getCurrentHiveId()).child('button').removeClass('on');
		Ext.get('switch-gold-annually' + Kohive.OS.getCurrentHiveId()).child('button').removeClass('on');
		
		var btn_id = billing_period ? 'switch-' + plan_type + '-' + billing_period  + Kohive.OS.getCurrentHiveId() : 'switch-' + plan_type  + Kohive.OS.getCurrentHiveId();
		var btn = Ext.get(btn_id);
		if (btn) btn.child('button').addClass('on');
		
		Ext.getCmp('personal_account_selected_plan_type' + Kohive.OS.getCurrentHiveId()).setValue(plan_type);
		Ext.getCmp('personal_account_selected_billing_period' + Kohive.OS.getCurrentHiveId()).setValue(billing_period);
		
		this.pricePlanChanged = true;
		
		if (plan_type == "free") this.hideBillingInformation();
		else this.showBillingInformation();
	},
	
	/**
	 * Called when the account form is submitted
	 */
	onChangePasswordUpdate: function() {
		var user      = Kohive.OS.getCurrentUser();
		var formPanel = this.changePasswordFormPanel;
		var fields    = changePasswordFormPanel.getForm().getValues();
		
		var password        = Ext.getCmp('personal_account_password' + Kohive.OS.getCurrentHiveId());
		var passwordConfirm = Ext.getCmp('personal_account_confirm_password' + Kohive.OS.getCurrentHiveId());
		var passwordOld     = Ext.getCmp('personal_account_old_password' + Kohive.OS.getCurrentHiveId());
		var email           = Ext.getCmp('personal_account_email' + Kohive.OS.getCurrentHiveId());
		
		// Check if password confirmation is filled in if password is filled in
		if (password.getValue() != "") {
			password.allowBlank = false;
			passwordOld.allowBlank = false;
			if (!passwordOld.isValid()) return;
			if (!passwordConfirm.isValid()) return;
			
			var index = Kohive.OS.getController('settings').getRunningView('settings-index' + Kohive.OS.getCurrentHiveId());
			index.showLoading();
		
			var controller = Kohive.OS.getController('users');
			if (controller) {
				controller.update(user, fields, {
					url: '/update_user_password'
				});
				controller.on('update', 			 this.onChangePasswordUpdateSuccess, this);
				controller.on('update-failed', this.onChangePasswordUpdateFailure, this);
			};
		};
	},
	
	/**
	 * Called when the update user form succeeds
	 */
	onChangePasswordUpdateSuccess: function() {
		//show a success status bar
		var index = Kohive.OS.getController('settings').getRunningView('settings-index' + Kohive.OS.getCurrentHiveId());
		index.showSuccess();
		
		//reset all the fields
		var password        = Ext.getCmp('personal_account_password' + Kohive.OS.getCurrentHiveId());
		var passwordConfirm = Ext.getCmp('personal_account_confirm_password' + Kohive.OS.getCurrentHiveId());
		var passwordOld     = Ext.getCmp('personal_account_old_password' + Kohive.OS.getCurrentHiveId());
		
		password.reset();
		passwordConfirm.reset();
		passwordOld.reset();
	},
	
	/**
	 * Called when the user update form fails
	 */
	onChangePasswordUpdateFailure: function(user, model) {
		var index = Kohive.OS.getController('settings').getRunningView('settings-index' + Kohive.OS.getCurrentHiveId());
		index.showFailure();
		
		var errors    = model.errors.forForm();
		var formPanel = this.changePasswordFormPanel;
		
		formPanel.getForm().markInvalid(errors);
	},
	
	/**
	 * Removes the billing information panel from view
	 */
	initBillingInformation: function() {
		var el = Ext.get('personal_account_billing_information' + Kohive.OS.getCurrentHiveId());
		el.enableDisplayMode();
		el.hide();
	},
	
	/**
	 * Slides the billing information into view
	 */
	showBillingInformation: function() {
		var el = Ext.get('personal_account_billing_information' + Kohive.OS.getCurrentHiveId());
		if (!el.isVisible()) el.slideIn();
	},
	
	/**
	 * Slides the billing information out of view
	 */
	hideBillingInformation: function() {
		var el = Ext.get('personal_account_billing_information' + Kohive.OS.getCurrentHiveId());
		if (el.isVisible()) el.slideOut('t', {useDisplay:true});
	},
	
	/**
	 * 
	 */
	onBillingInformationUpdate: function() {
	  var controller  = Kohive.OS.getController('settings'),
	      indexWindow = controller.indexWindow;
	  
	  indexWindow.showLoading();
	  
	  this.billingInformationFormPanel.form.submit({
	    url:    '/subscriptions/1',
	    method: 'PUT',
	    scope:  this,
	    success: function() {
	      indexWindow.showSuccess();
	      this.hideBillingInformation();
	      Kohive.OS.loadSession();
	    },
	    failure: function(form, action) {
	      indexWindow.showFailure();
	      Ext.Msg.alert('Error', 'We\'re sorry, your payment request returned the following error(s):<br /><br />' + Ext.decode(action.response.responseText).errors.join('.<br />') + '<br /><br />Please try again. If you continue to have problems please contact <a href="mailto:support@kohive.com">support@kohive.com</a>.');
	    }
	  });
	},
	
	initCurrency: function() {
	  this.poundBtn  = this.el.child('.bottom .pound');
		this.dollarBtn = this.el.child('.bottom .dollar');
		this.euroBtn   = this.el.child('.bottom .euro');
		
		this.poundBtn.on('click',  function() { this.selectCurrency('pound'); }, this);
		this.dollarBtn.on('click', function() { this.selectCurrency('dollar'); }, this);
		this.euroBtn.on('click',   function() { this.selectCurrency('euro'); }, this);
	},
	
	selectCurrency: function(name) {
	  this.poundBtn.removeClass('active');
	  this.dollarBtn.removeClass('active');
	  this.euroBtn.removeClass('active');
	  
	  this[name + 'Btn'].addClass('active');
	  
	  if (name == "dollar") this.setCurrencyDollar();
    if (name == "pound")  this.setCurrencyPound();
    if (name == "euro")   this.setCurrencyEuro();
	},
	
	setCurrencyDollar: function() {
	  this.bronzeMonthlyPrice.update('$15');
		this.bronzeAnnuallyPrice.update('$150');
		this.silverMonthlyPrice.update('$30');
		this.silverAnnuallyPrice.update('$300');
		this.goldMonthlyPrice.update('$75');
		this.goldAnnuallyPrice.update('$750');
	},
	
	setCurrencyPound: function() {
	  this.bronzeMonthlyPrice.update('&pound;10');
		this.bronzeAnnuallyPrice.update('&pound;100');
		this.silverMonthlyPrice.update('&pound;20');
		this.silverAnnuallyPrice.update('&pound;200');
		this.goldMonthlyPrice.update('&pound;50');
		this.goldAnnuallyPrice.update('&pound;500');
	},
	
	setCurrencyEuro: function() {
	  this.bronzeMonthlyPrice.update('&euro;11');
		this.bronzeAnnuallyPrice.update('&euro;110');
		this.silverMonthlyPrice.update('&euro;22');
		this.silverAnnuallyPrice.update('&euro;220');
		this.goldMonthlyPrice.update('&euro;55');
		this.goldAnnuallyPrice.update('&euro;550');
	}
	
	// onDestroy: function(msg) {
	// 	if (msg == "no") return;
	// 	if (msg != "yes") {
	// 		return Ext.Msg.show({
	//       title:   '',
	//       msg:     Ext.ux.Translator.translate('destroy_account_desc'),
	//       buttons: {yes: Ext.ux.Translator.translate('destroy_account'), no: Ext.ux.Translator.translate('keep_account')},
	// 			scope:   this,
	//       fn:      this.onDestroyConfirm
	//     });
	// 	};
	// 	
	// 	// Destroy the user
	// 	var user = Kohive.OS.getCurrentUser();
	// 	var controller = Kohive.OS.getController('users');
	// 	
	// 	if (user && controller) controller.destroy(user);
	// 	else Ext.Msg.alert('', '306:' + Ext.ux.Translator.translate('something_wrong'));
	// },
	// 
	// onDestroyConfirm: function(msg) {
	// 	if (msg != "yes") return;
	// 	
	// 	return Ext.Msg.show({
	//       title:   '',
	//       msg:     Ext.ux.Translator.translate('destroy_account_confirm_desc'),
	//       buttons: {yes: Ext.ux.Translator.translate('destroy_account'), no: Ext.ux.Translator.translate('keep_account')},
	// 		scope:   this,
	//       fn:      this.onDestroy
	//     });
	// }
});

Ext.reg('settings_view_personal_account', Kohive.views.settings.cards.PersonalAccount);

/**
 * Kohive.views.settings.cards.PersonalNotifications
 * @extends Ext.form.FormPanel
 * Settings config options for Personal Notification settings
 */
Kohive.views.settings.cards.PersonalNotifications = function(config) {
  var config = config || {};
	
	Ext.applyIf(config, {
    id:         'settings_view_personal_notifications' + Kohive.OS.getCurrentHiveId(),
    cls:        'settings_view_hive_appearance',
    title:      Ext.ux.Translator.translate('hive') + ' : ' + Ext.ux.Translator.translate('notifications'),
    header:     false,
    autoScroll: true,
    
    items:  [
      {
        xtype:      'fieldset',
        title:      Ext.ux.Translator.translate('email'),
        autoHeight: true,
				defaults: {
					border: false
				},
        items: [
					{
						layout: 'column',
						height: 20,
						defaults: {
							border: false
						},
						items: [
							{
								columnWidth: .6,
								width:       180,
								html:        Ext.ux.Translator.translate('notifications_invitation_to_hive')
							},
							{
								columnWidth: .4,
								width:       22,
								items: [
									new Kohive.IconButton({
								    iconCls:  'switch',
								    scope:    this,
								    handler:  this.onUpdate,
										disabled: true,
										onRender: function(ct, position) {
								      Kohive.IconButton.superclass.onRender.call(this, ct, position);

											var membership = Kohive.OS.getCurrentHive();
											if (membership.get('email_notifications') === "true") this.el.child('button').addClass('on');
											else this.el.child('button').removeClass('on');
								    }
								  })
								]
							}
						]
					},
					{
						layout: 'column',
						height: 20,
						defaults: {
							border: false
						},
						items: [
							{
								columnWidth: .6,
								width:       180,
								html:        Ext.ux.Translator.translate('notifications_kicked_from_hive')
							},
							{
								columnWidth: .4,
								width:       22,
								items: [
									new Kohive.IconButton({
								    iconCls:  'switch',
								    scope:    this,
								    handler:  this.onUpdate,
										disabled: true,
										onRender: function(ct, position) {
								      Kohive.IconButton.superclass.onRender.call(this, ct, position);

											var membership = Kohive.OS.getCurrentHive();
											if (membership.get('email_notifications') === "true") this.el.child('button').addClass('on');
											else this.el.child('button').removeClass('on');
								    }
								  })
								]
							}
						]
					}
				]
      }
    ]
  });
 
	Kohive.views.settings.cards.PersonalNotifications.superclass.constructor.call(this, config);
  
	this.addEvents(
	  /**
	   * @event update
	   * Fires when the notifications form is updated
	   * @param {Ext.form.FormPanel} formPanel The form panel instance
	   * @param {Ext.form.BasicForm} form The form being submitted
	   */
	  'update'
	);
};
Ext.extend(Kohive.views.settings.cards.PersonalNotifications, Ext.Panel, {
	/**
	 * Called when the email notification button is clicked
	 */
	onUpdate: function(btn) {
		// // Check if the button is enabled or not
		// var btnEl   = this.el.child('button'),
		//     enabled = btnEl.hasClass('on');
		// 
		// var membership = Kohive.OS.getCurrentHive();
		// 
		// this.showUpdating();
		// 
		// if (enabled) {
		// 	membership.set('email_notifications', false);
		// 	membership.save({
		// 		scope:   this,
		// 		success: function() {
		// 			btnEl.removeClass('on');
		// 			this.showSuccess();
		// 		}
		// 	});
		// } else {
		// 	membership.set('email_notifications', true);
		// 	membership.save({
		// 		scope:   this,
		// 		success: function() {
		// 			btnEl.addClass('on');
		// 			this.showSuccess();
		// 		}
		// 	});
		// };
	},
	
	showUpdating: function() {
		var v = Ext.getCmp('settings-index' + Kohive.OS.getCurrentHiveId());
		v.showUpdating();
	},
	
	showSuccess: function() {
		var v = Ext.getCmp('settings-index' + Kohive.OS.getCurrentHiveId());
		v.showSuccess();
	}
});

Ext.reg('settings_view_personal_notifications', Kohive.views.settings.cards.PersonalNotifications);

/**
 * Kohive.views.settings.cards.PersonalLanguage
 * @extends Ext.form.FormPanel
 * 
 */
Kohive.views.settings.cards.PersonalLanguage = function(config) {
  var config = config || {};
  
  // Get the os, controller and the main index view
  var os          = ExtMVC.OS.getOS();
  this.controller = os.getController('settings');
	
	var language = Kohive.OS.getCurrentUser().get('language');
	
  /**
   * @property store
   * @type Ext.data.Store
   * The store bound to this view's DataView.  Read-only
   */
  this.standardStore = new Ext.data.SimpleStore({
    fields: ['icon', 'title', 'selected'],
    data:   [
      ['en-GB', 'english',    (language == 'en-GB') ? true : false],
      ['pt-BR', 'portuguese', (language == 'pt-BR') ? true : false]
    ]
  });
  
	var tpl = new Ext.XTemplate(
    '<tpl for=".">',
      '<div class="x-application">',
				'<div class="x-icon"><img src="/images/apps/Settings/icons/language/{icon}.png" /></div>',
				'<div class="x-title">',
					'{[Ext.ux.Translator.translate(values.title)]}',
				'</div>',
        '<div class="x-button">',
					'<div class="kohive-icon-button green x-kohive-icon-button" style="width: auto;"><button class="switch<tpl if="selected"> on</tpl>"></button></div>',
				'</div>',
      '</div>',
    '</tpl>'
 	);
	
  /**
   * @property datastore
   * @type Ext.DataView
   * Custom dataview for displaying the ID Cards.
   */
  this.standardDataView = new Ext.DataView({
    autoHeight:   true,
    overClass:    'x-view-over',
    border:       'false',
    itemSelector: 'div.x-application',
    store:        this.standardStore,
    tpl:          tpl,
    listeners: {
			'click': {
				scope: this,
				fn: this.delegateClick
			}
    }
  });
  
  Ext.applyIf(config, {
		id:         'settings_view_personal_language' + Kohive.OS.getCurrentHiveId(),
    cls:        'settings_view_hive_applications',
    title:      Ext.ux.Translator.translate('personal') + ' : ' + Ext.ux.Translator.translate('language'),
    header:     false,
    autoScroll: true,
    
		items: [
			this.standardDataView
		]
  });
 
  Kohive.views.settings.cards.PersonalLanguage.superclass.constructor.call(this, config);
	
  // this.initListeners();
};
Ext.extend(Kohive.views.settings.cards.PersonalLanguage, Ext.form.FormPanel, {
	delegateClick: function(dataview, index, node, e) {
		var el     = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
		
		//check if it is the switch that has been clicked or it is disabled
		if (!el.hasClass('switch')) return;
		if (el.hasClass('x-disabled')) return;
		
		//update the record
		if (!record.get('selected')) {
		  var store = dataview.store;
		  store.each(function(rec) {
		    rec.set('selected', false);
		  }, this);
		  
		  //update the selected record
		  record.set('selected', true);
		};
		
		var user = Kohive.OS.getCurrentUser();
		user.set('language', record.get('icon'));
		
		this.controller.indexWindow.showLoading();
		
    //save the user
    user.save({
     scope:   this,
     success: function() {
       this.controller.indexWindow.showSuccess();
       window.location.reload();
     },
     failure: function() {
       this.controller.indexWindow.showFailure();
     }
    });
	}
});

Ext.reg('settings_view_personal_language', Kohive.views.settings.cards.PersonalLanguage);

/**
 * Kohive.views.settings.cards.PersonalWidgets
 * @extends Ext.form.FormPanel
 * Allows additional and removal of widgets from a Hive
 */
Kohive.views.settings.cards.PersonalWidgets = function(config) {
  var config = config || {};
 
  Ext.applyIf(config, {
    id:     'settings_view_personal_widgets' + Kohive.OS.getCurrentHiveId(),
    title:  'Personal : Widgets',
    header: false,
    
    items: [
      {
        xtype:      'textfield',
        fieldLabel: 'This is not here',
        name:       'frell',
        value:      'Seriously'
      }
    ]
  });
 
  Kohive.views.settings.cards.PersonalWidgets.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.views.settings.cards.PersonalWidgets, Ext.form.FormPanel);

Ext.reg('settings_view_personal_widgets', Kohive.views.settings.cards.PersonalWidgets);

/**
 * @class Kohive.views.videos.Viewer
 * @extends Ext.Panel
 * Displays an album selector and the videos for the selected album
 * @cfg {Ext.data.Store} albumStore The Ext.data.Store subclass to use for the albums combo box (required)
 */
Kohive.views.videos.Viewer = Ext.extend(Ext.Panel, {
	/**
	 * @property deletingAlbum
	 * @type Boolean
	 * Tru when the user is deleting an album
	 */
  deletingAlbum: false,
	
	
  initComponent: function() {
    /**
     * @property controller
     * @type ExtMVC.Controller
     * A reference to the controller controlling this view
     */
    this.controller = Kohive.OS.getController('videos');
    
    /**
     * @property hiveId
     * @type Number
     * A reference to the Hive ID this viewer is for.  This is set to the ID of the current hive when this component
     * is created.  It is required because we only want to listen to events from the videos controller when they are
     * fired for this particular hive.  Each hive has its own instance of the videos application so we only listen
     * to events from the relevant hive.
     */
    this.hiveId = Kohive.OS.getCurrentHive().get('hive_id');
		
    /**
     * @property albumsCombo
     * @type Ext.form.ComboBox
     * The album selector combo box
     */
    this.albumsCombo = new Ext.form.ComboBox({
      cls:            'videos-combobox',
      mode:           'local',
      store:          this.albumStore,
      editable:       false,
      triggerAction:  'all',
      displayField:   'title',
      valueField:     'id',
      emptyText:      Ext.ux.Translator.translate('app_statusbar_loading'),
      selectOnFocus:  true,
      width:          160,
      maxHeight:      200,
			tpl:            new Ext.XTemplate(
				'<tpl for=".">',
					'<div class="x-combo-list-item">',
						'<tpl if="xcount &gt; 1"><tpl if="can_delete"><div class="x-delete"></div></tpl></tpl>',
						'{title}',
					'</div>',
				'</tpl>'
			)
    });
    
    /**
     * @property albumField
     * @type Ext.form.TextField
     * Description
     */
    this.albumField = new Ext.form.TextField({
      cls:             'app-album-field',
      name:            'title',
      width:           160,
      height:          19,
      enableKeyEvents: true,
      hidden:          true,
      allowBlank:      false,
      validationEvent: false
    });
    
    /**
     * @property addBtn
     * @type Kohive.Button
     * Description
     */
    this.addBtn = new Kohive.IconButton({
      cls:     'videos-button add',
      iconCls: 'new-album',
      width:   11,
      scope:   this
    });
    

    /**
     * @property albumManager
     * @type Ext.Panel
     * A panel which has a combobox to display all the albums, also has buttons
     * to add and destroy albums.
     */
    this.albumManager = new Ext.Panel({
      cls:    'x-app-album-manager',
      region: 'north',
			border: false,
      height: 19,
      items:  [
        this.albumsCombo,
        this.albumField,
        this.addBtn
      ]
    });
    
    /**
     * @property videosDataView
     * @type Ext.DataView
     * DataView used to render the videos for the current album
     */
    this.videosDataView = new Ext.DataView({
			cls:            'x-app-viewer',
      emptyText:      '<div class="x-image-blank"></div>',
      deferEmptyText: false,
      singleSelect:   true,
      autoHeight:     true,
      autoScroll:     true,
      overClass:      'x-over',
      itemSelector:   'div.thumbnail',
      tpl:            new Ext.XTemplate(
        '<tpl for=".">',
          '<div class="thumbnail">',
						'<tpl if="can_delete"><div class="x-delete"></div></tpl>',
            '<img src="{thumbnail_filename}" />',
          '</div>',
        '</tpl>'
      )
    });
    
    /**
     * @property videoScroller
     * @type Ext.Panel
     * A scroller which displays all videos in the current album
     */
    this.videoScroller = new Ext.Panel({
      cls:        'x-videos-scroller',
      region:     'center',
			border:     false,
      autoScroll: true,
      items:      [this.videosDataView]
    });
    
    Ext.applyIf(this, {
			border: false,
			layout: 'border',
      items:  [
        this.albumManager,
        this.videoScroller
      ]
    });
    
    Kohive.views.videos.Viewer.superclass.initComponent.apply(this, arguments);
    
    this.initEvents();
    this.initListeners();
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event album-selected
       * Fires when an album is selected in the combo box
       * @param {Kohive.models.Album} album The selected Album
       */
      'album-selected',
      
      /**
       * @event create-album
       * Fires when the user has entered and album name and clicked the 'Add' button or pressed enter
       * @param {String} albumName The name of the album to create
       */
      'create-album',
      
      /**
       * @event create-rss-album
       * Fires when the user has entered the feed url and clicked the 'Add' button or pressed enter
       * @param {String} feedUrl The url of the feed to create
       */
      'create-rss-album',
      
      /**
       * @event video-clicked
       * Fired when a video in the videos DataView is clicked
       * @param {Kohive.models.Video} video The video that was clicked
       */
      'video-clicked',
			
			/**
			 * @event destroy-video
			 * Fires when the user wants to destroy a video
			 * @param {Kohive.models.Video} video The video that was clicked
			 */
			'destroy-video',
			
			/**
			 * @event load-album
			 * Fires when the album needs to be loaded
			 * @param {Kohive.models.Album} album The album that needs to be loaded
			 */
			'load-album'
    );    
  },

  /**
   * Attaches listeners to the controller child elements
   */
  initListeners: function() {
    this.controller.on({
      scope:                this,
      'videos-loaded':      this.reloadVideos,
      'update':             this.updateStore,
      'album-created':      this.onAlbumCreated,
      'rss-album-created':  this.onRssAlbumCreated,
      'album-destroyed':    this.onAlbumDestroyed
    });
    
    this.albumsCombo.on(   'select',       this.onComboSelect, this);
    this.albumsCombo.on(   'click',        this.onComboClick, this);
    this.albumsCombo.on(   'beforeselect', this.canSelectAlbum, this);
    this.addBtn.on(        'click',        this.onAddButtonClick, this);
    this.albumField.on(    'keyup',        this.onSpecialKey, this);
    this.videosDataView.on('click',        this.onPhotoClick, this);
    
    var firstTime = true;
    this.albumStore.on('load', function(store, records, options) {
      if (firstTime) {
				// Select the first album in the combo + fire off the select event as it isn't
				// fired when using setValue
				this.albumsCombo.setValue(records[0].get('id'));
				this.albumsCombo.fireEvent('select', this, records[0], 0);
				
				// Enable the upload and refresh buttons
				// Ext.getCmp('upload_video_button').enable(); we dont have to enable this, as it gets overlayed with flash
				var btn = Ext.getCmp('refresh_video_button' + Kohive.OS.getCurrentHiveId());
				if (btn) btn.enable();
				
				firstTime = false;
				
				return;
			};
    }, this);
  },
  
	updateStore: function(video) {
		var store = this.videosDataView.getStore();
		if (!store) return;
		
		var recId = null;
		
		store.each(function(record, id) {
			if (record.get('id') == video.get('id')) recId = id;
		}, this);
		
		//update the record
		this.videosDataView.store.data.items[recId] = video;
	},
	
  /**
   * Called when the Add button is clicked.  Hides and shows albums combo and new album field
   * as appropriate
   */
  onAddButtonClick: function() {
    if (this.enteringNewAlbum()) {
      this.onCreateAlbum();
    } else {
      this.toggleNewAlbumForm();
    }
  },
  
  /**
   * Toggles between showing the existing albums combo and the new album form
   */
  toggleNewAlbumForm: function() {
    var button = this.addBtn.el.child('button');
        
    //toggle visibility of the textfield and the combo box
    Ext.each([this.albumField, this.albumsCombo], function(f) {f.setVisible(f.hidden);});
    button.toggleClass('new-album').toggleClass('submit-album');
    
    //select textfield if adding
    if (this.albumField.isVisible()) this.albumField.focus();
  },
  
  /**
   * Returns true if the user is currently entering a new album name
   * @return {Boolean} True if the user is currently entering a new album name
   */
  enteringNewAlbum: function() {
    return this.albumsCombo.hidden;
  },
  
  /**
   * Called when the Add button is clicked while in new album mode, or when the enter key is pressed
   * inside the album field.
   */
  onCreateAlbum: function() {
		// Check if the value is empty or not
		if (!this.albumField.isValid()) return this.toggleNewAlbumForm();
		
    this.fireEvent('create-album', this.albumField.getValue());
  },
  
  /**
   * Called when the Add button is clicked while in new rss album mode, or when the enter key is pressed
   * inside the album field.
   */
  onCreateRssAlbum: function() {
		// Check if the value is empty or not
    if (!this.rssAlbumField.isValid()) return this.toggleNewRssAlbumForm();
    
    this.fireEvent('create-rss-album', this.rssAlbumField.getValue());
  },
  
  /**
   * Called when the user chooses a new item in the albums combo box
   */
  onComboSelect: function(combo, record, index) {
    this.fireEvent('album-selected', record);
  },

	/**
   * Called when the user clicks anywhere in the combo box.
   */
  onComboClick: function(dataview, index, node, e) {
		// Get the target element
		var el = Ext.get(e.getTarget());
		
		// Get the album
		var album = dataview.getRecord(node);
		
		// Check if it has the delete class
		if (el.hasClass('x-delete')) this.onAlbumDelete(album);
  },
  
  /**
   * Called when ENTER or ESC keys are pressed while new album field is focused
   * @param {Ext.form.Field} field The field firing the event
   * @param {Event} e The keypress event
   */
  onSpecialKey: function(field, e) {
    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) this.onCreateAlbum();    
    if (e.getKey() == e.ESC) this.toggleNewAlbumForm();
  },
  
  onRssAlbumFieldSpecialKey: function(field, e) {
    if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) this.onCreateRssAlbum();
    if (e.getKey() == e.ESC) this.toggleNewRssAlbumForm();
  },
  
  /**
   * Called when a video in the dataview is clicked.  Emits a video-clicked event
   * @param {Ext.DataView} dataview The videos dataview
   * @param {Number} index The index of the clicked video
   * @param {HTMLElement} node The node that was clicked on
   */
  onPhotoClick: function(dataview, index, node, e) {
		// Get the target element the user clicked on
		var el = Ext.get(e.getTarget());
		
		// Get the video record using the node the user clicked on (always the thumbnail)
    var video = dataview.getRecord(node);
		
		// check if it has the x-delete class
		if (el.hasClass('x-delete')) return this.onDelete(video);
		
		// It doesn't have the delete class, so just fire the video clicked event 
		this.fireEvent('video-clicked', video);
  },
	
	/**
	 * Returns false if the user is deleting an album
	 */
	canSelectAlbum: function() {
		if (this.deletingAlbum) return false;
		
		return true;
	},
  
  /**
   * Called when the controller indicates a new album has been successfully created
   * Refreshes the combo store and calls toggleNewAlbumForm
   * @param {Kohive.models.Album} album The newly created album
   */
  onAlbumCreated: function(album) {
		this.ifCurrentHive(function() {
			this.albumField.reset();
			if (this.albumField.isVisible()) this.toggleNewAlbumForm();
			
			this.albumsCombo.store.on('load', this.showLastAlbum, this);
			this.albumsCombo.store.reload();
		});
  },
  
  /**
   * Called when the controller indicates a new rss album has been successfully created
   * Refreshes the combo store and calls toggleNewRssAlbumForm
   * @param {Kohive.models.Album} album The newly created album
   */
  onRssAlbumCreated: function(album) {
		this.ifCurrentHive(function() {
			this.rssAlbumField.reset();
			this.toggleNewRssAlbumForm();
			
			this.albumsCombo.store.on('load', this.showLastAlbum, this);
			this.albumsCombo.store.reload();
		});
  },


	/**
	 * Called when the controller says an album has been destroyed
   * @param {Kohive.models.Album} album The newly destroyed album
	 */
	onAlbumDestroyed: function(album) {
		var currentAlbum = this.currentAlbum[Kohive.OS.getCurrentHiveId()];
		
		if (currentAlbum.get('id') == album.get('id')) this.showFirstAlbum();
		
		this.albumsCombo.store.reload();
	},
	
	/**
	 * Shows the last album from the alabum combobox
	 */
	showLastAlbum: function(store) {
		var store = store || this.albumsCombo.store;
		
		this.albumsCombo.store.un('load', this.showLastAlbum, this);
		
		var album = store.getAt(store.data.items.length - 1);
		if (album) this.fireEvent('load-album', album);
	},
	
	/**
	 * Shows the first album from the combobox
	 */
	showFirstAlbum: function() {
		var store = this.albumsCombo.store;
		
		var album = store.getAt(0);
		if (album) this.fireEvent('load-album', album);
	},
	
	/**
	 * Called when an album is delete from the combobox
	 * @param {Kohive.models.Album} album Instance of the album which needs to be deleted
	 */
	onAlbumDelete: function(album) {
		this.deletingAlbum = true;
		
		return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('destroy_videoalbum_desc', { title: album.get('title')}),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
          this.fireEvent('destroy-album', album);
        };
				
				this.deletingAlbum = false;
      }   
    });
	},
	
	/**
	 * 
	 * @param {Kohive.models.Photo} video Instance of the video which needs to be deleted
	 */
	onDelete: function(video) {
		return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('destroy_video_desc', { title: video.get('title')}),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
          this.fireEvent('destroy-video', video);
        }
      }   
    });
	},
  
  /**
   * Updates the Videos list with a new store
   * @param {Ext.data.Store} store The store containing videos to display
   * @param {Kohive.models.Album} album The Album whose videos have been loaded 
   */
  reloadVideos: function(store, album) {
    this.ifCurrentHive(function() {
      this.videosDataView.setStore(store);
      this.albumsCombo.setValue(album.get('id'));      
    });
  },
  
  /**
   * Runs a function if the current hive's ID matches this.hiveId
   * @param {Function} fcn A function to run if the current hive matches
   * @param {Object} scope The scope to execute the function in (defaults to the Viewer scope)
   */
  ifCurrentHive: function(fcn, scope) {
    var currentHiveId = Kohive.OS.getCurrentHive().get('hive_id');
    
    if (currentHiveId == this.hiveId) fcn.call(scope || this);
  }
});

Ext.reg('videos_viewer', Kohive.views.videos.Viewer);

/**
 * @class Kohive.views.videos.Index
 * @extends Kohive.Window
 * The main videos application window - houses albums panel and an video panel
 */
Kohive.views.videos.Index = Ext.extend(Kohive.Window, {
  title:    Ext.ux.Translator.translate('app_videos'),
  cls:      'x-videos-index',
  url:      'videos',
  layout:   'border',
  width:    800,
	minWidth: 800,
  height:   400,
  defaults: {border: false},
 
  /**
   * Creates the sub-view elements
   */
  initComponent: function() {
    /**
     * @property videoView
     * @type Ext.Panel
     * Reference to the video view
     */
    this.videoView = new Kohive.views.videos.Video({
      hidden: true,
      id:     'videos-viewer' + Kohive.OS.getCurrentHiveId(),
      listeners: {
        scope:  this,
        'delete': function(video) {
          this.showDestroying();
          this.fireEvent('destroy-video', video);
        },
        'update': function(video, fields, forceRefresh) {
          this.showUpdating();
          this.fireEvent('update-video', video, fields, forceRefresh);
        }
      }
    });
		
		/**
		 * @property newVideoView
		 * @type Kohive.views.videos.New
		 * Reference to the new vudei from Url panel
		 */
		this.newVideoView = new Kohive.views.videos.New();
    
    /**
     * @property commentsNew
     * @type Ext.Panel
     * Reference to the new comment view
     */
    this.commentsNew = new Kohive.views.comments.New({
      hidden: true,
      id:     'comments-new-videos' + Kohive.OS.getCurrentHiveId()
    });

    /**
     * @property commentsIndex
     * @type Ext.Panel
     * Reference to the comments view
     */
    this.commentsIndex = new Kohive.views.comments.Index({
      hidden: true,
      autoReloadComments: true,
      id:     'comments_index_videos' + Kohive.OS.getCurrentHiveId()
    });
    
    /**
     * @property photoViewer
     * @type Kohive.views.videos.Viewer
     * Reference to the photo viewer panel
     */
    this.photoViewer = new Kohive.views.videos.Viewer({
      cls:        'x-menu',
      albumStore: this.albumStore,
      listeners:  {
        scope: this,
        'album-selected': function(album) {
          this.fireEvent('load-album', album);
        },
        'video-clicked': function(photo) {
          this.fireEvent('show-video', photo);
        }
      }
    });
    
    this.relayEvents(this.photoViewer, ['create-album', 'create-rss-album', 'destroy-video', 'destroy-album', 'load-album']);
    
		var controller = Kohive.OS.getController('videos');
		
    Ext.apply(this, {
      plugins:    [new Ext.ux.HeaderToolbar()],
      hbar: [
        {
          text:     Ext.ux.Translator.translate('url'),
          id:       'new_video' + Kohive.OS.getCurrentHiveId(),
          iconCls:  'newurl',
          xtype:    'header_toolbar_button',
          scope:    this,
					handler: function() {
						var f = this.newVideoView;
						f.toggleCollapse();
			      f.doLayout();

						// Get the first item and focus on it
						var field = f.find()[0];
						if (field) {
							var tempTask = new Ext.util.DelayedTask(function() {
							  field.focus();
							}, this);
							tempTask.delay(500);
						};
					}
        },
        {
          text:     Ext.ux.Translator.translate('refresh'),
          id:       'refresh_video_button' + Kohive.OS.getCurrentHiveId(),
          iconCls:  'refresh',
          xtype:    'header_toolbar_button',
					disabled: true,
          handler: function() { 
            this.fireEvent('refresh');
          },
          scope:   this
        }// ,
        // '->',
        // new Ext.ux.HeaderToolbarButton({
        //              text:    Ext.ux.Translator.translate('flickr_sync'),
        //              iconCls: 'flickr',
        //  disabled: true
        //            })
      ],
      bbar:     true,
      filterPanelConfig: {
        filters: [
          {
            ftype:        'store-helper',
            displayField: 'tags.name',
            valueField:   'tags.name',
            title:        Ext.ux.Translator.translate('tags'),
            iconCls:      'tag',
            view:         controller,
            listenTo: {
              component: controller,
              events:    ['videos-loaded']
            }
          },
          {
            ftype:        'store-helper',
            displayField: 'author_nickname',
            valueField:   'author_membership_id',
            title:        Ext.ux.Translator.translate('authors'),
            iconCls:      'author',
            view:         controller,
            listenTo: {
              component: controller,
              events:    ['videos-loaded']
            }
          }
        ],
        listeners: {
          scope            : this,
          'filters-updated': this.onFiltersUpdated
        }
      },
      id:    'videos_index_' + Kohive.OS.getCurrentHiveId(),
      items: [
        {
          cls:         'x-menu-wrap',
          region:      'west',
          width:       180,
          collapsible: false,
          layout:      'fit',
          items:       [this.photoViewer]
        },
        {
          region:     'center',
          cls:        'x-content-wrap',
          autoScroll: true,
          items: [
						this.newVideoView,
            this.videoView,
            this.commentsIndex,
            this.commentsNew
          ]
        }
      ]
    });
    
    Kohive.views.videos.Index.superclass.initComponent.apply(this, arguments);
    
    this.initListeners();
  },
  
  /**
   * Called when the videos-loaded event is fired by the controller (e.g. new album loaded).
   * This sets up the flash upload for this album
   * @param {Ext.data.Store} videos The videos to load
   * @param {Kohive.models.Album} album The album that has been loaded
   */
  onVideosLoaded: function(videos, album) {
		this.hideLoading();
		
		//init the tag store
		this.initTagsStore(videos);
  },
	
	/**
	 * Shows success message on the window and hides the new photo view
	 */
	closeNewVideoView: function() {
		this.showSuccess();
		this.newVideoView.toggleCollapse();
	},
	
  /**
   * Called when the controller fires its show-video event. This tells the comments form and index
   * to show themselves
   * @param {Kohive.models.Video} video The video to show
   */
  onShowVideo: function(video, newVideo) {
		this.commentsIndex.hide();
		this.commentsNew.hide();
		
    this.commentsIndex.setReferenceObject(video);
    this.commentsNew.setReferenceObject(video);
		
    if (video) {
		  var tempTask = new Ext.util.DelayedTask(function() {
			  this.commentsNew.show();
			  this.commentsNew.el.hide();
			  this.commentsNew.el.fadeIn({duration:0.3});
			}, this);
			tempTask.delay(300);
		};
  },
  
  /**
   * Called whenever the user changes the filters applied in the filter bar. Fires the 'filter' event
   * @param {Object} filtersApplied The object of currently applied filters.  Something like:
   * {authors: [1, 2, 5], tags: ['cat', 'dog']}
   */
  onFiltersUpdated: function(filtersApplied) {
    this.fireEvent('filter', filtersApplied);
  },
  
  /**
   * Called when a new album has been created by the controller
   * @param {Kohive.models.Album} album The newly created album
   */
  onAlbumCreated: function(album) {
		this.hideLoading();
  },
  
  /**
   * Called when a new rss album has been created by the controller
   * @param {Kohive.models.Album} album The newly created album
   */
  onRssAlbumCreated: function(album) {
		this.hideLoading();
  },
  
  /**
   * Listen to events fired by the controller and sub-views
   */
  initListeners: function() {
    var controller = Kohive.OS.getController('videos');
    
    controller.on({
      scope: this,
      'before-create-video'  : this.showCreating.createDelegate(this, [Ext.ux.Translator.translate('app_statusbar_uploading')]),
      'before-create-album'  : this.showCreating.createDelegate(this, [Ext.ux.Translator.translate('app_statusbar_uploading')]),
      'before-destroy-album' : this.showDestroying.createDelegate(this, []),
      'album-created'        : this.onAlbumCreated,
      // 'rss-album-created'    : this.onRssAlbumCreated,
      'album-destroyed'      : this.showSuccess,
      'update'               : this.showSuccess,
      'video-destroyed'      : this.showSuccess,
      'create-video-failed'  : this.showSuccess.createDelegate(this, []),
      'before-load-videos'   : this.showRefreshing,
      'videos-loaded'        : function (videosStore, album) {
        this.originalStore = Kohive.OS.getController('videos').originalStore;
        this.onVideosLoaded(videosStore, album);
        this.onStoreLoad(videosStore);
       },
      'video-created'        : this.closeNewVideoView,
      'show-video'           : this.onShowVideo
    });
    
		this.on('show', function() {
			this.fireEvent('refresh', this);
		}, this);
		
		controller.on('videos-loaded', function(videos, album) {
		  this.fireEvent('update-filter-bar', album);
		}, this);
		controller.on('before-videos-loaded', function(album) {
		  this.fireEvent('update-filter-bar', album);
		}, this);
		controller.on('update', function() {
      this.fireEvent('update-filter-bar', controller.currentAlbum[Kohive.OS.getCurrentHiveId()]);
	  }, this);
  },
  
  onStoreLoad: function (store) {
  },
  
  /**
   * Sets up all events emitted by this component.
   * This is called by the superclass so no need to call it from initComponent or constructor
   */
  initEvents: function() {
    Kohive.views.videos.Index.superclass.initEvents.apply(this, arguments);
    
    this.addEvents(
      /**
       * @event create-album
       * Fires when the user attempts to create a new album
       * This is relayed from the Viewer sub-view component
       */

      /**
       * @event create
       * Fires when the user attempts to upload (create) an video
       */
      'create',
      
      /**
       * @event refresh
       * Fires when the user clicks the refresh button
       */
      'refresh',
      
      /**
       * @event update
       * Fires when the user wishes to update an Video
       * @param {Kohive.models.Video} video The Video to update
       * @param {Object} fields The fields object to update with
       */
      'update-video',
      
      /**
       * @event load-album
       * Fires when the user attempts to load an album and its videos
       */
      'load-album',
      
      /**
       * @event destroy-video
       * Fires when the user attempts to destroy an video
       */
      'destroy-video',
      
      /**
       * @event upload-started
       * Fires when a flash-powered upload begins
       */
      'upload-started',
      
      /**
       * @event upload-complete
       * Fires when a flash-powered upload is complete
       */
      'upload-complete',
      
      /**
       * @event update-filter-bar
       * Fires when the attached filter bar needs to update itself as a new album is being viewed
       * @param {ExtMVC.Model.Base} album The album that was just loaded
       */
      'update-filter-bar'
    );
  },
	
  /**
   * @property sessionInfo
   * @type Kohive.models.Session|Null
   * A cached copy of the Session model instance.  Don't access directly, use getSessionInfo instead
   */
  sessionInfo: null,
  
  /**
   * Calls back with a Session model object. If the session has not already been loaded it is
   * loaded first
   * @param {Function} callback The function to call when the session data has been loaded
   * This will be called with a single argument - the Kohive.model.Session model object
   * @param {Function} scope The scope to call the callback in (optional)
   */
  getSessionInfo: function(callback, scope) {
    scope = scope || this;
    
    if (this.sessionInfo == null) {
      Kohive.models.Session.find(1, {
        url:     '/session/get',
        scope:   this,
        success: function(session) {
          this.sessionInfo = session;
          callback.call(scope, this.sessionInfo);
        }
      });
    } else callback.call(scope, this.sessionInfo);
  }
});

Ext.reg('kohive_app_videos_view_index', Kohive.views.videos.Index);

/**
 * @class Kohive.views.videos.New
 * @extends Ext.form.FormPanel
 * Base class for any scaffold form panel (e.g. new and edit forms)
 */
Kohive.views.videos.New = function(config) {
  var config = config || {};
  
	this.textField = new Ext.form.TextField({
    fieldLabel: Ext.ux.Translator.translate('video_url'),
    name:       'import_string',
    allowBlank: false,
    anchor:     '-10'
  });
	
  Ext.applyIf(config, {
    title:          Ext.ux.Translator.translate('nnew'),
    controllerName: 'videos',
		items: [
			this.textField
		],
    buttons: [
      {
        text:    Ext.ux.Translator.translate('embed'),
        scope:   this,
        handler: this.onSubmit
      }
    ]
  });
  
  Kohive.views.videos.New.superclass.constructor.call(this, Kohive.models.Video, config);
  
	this.initListeners();
	
  // Call a method before the form is submitted
  this.getForm().on('beforeaction', this.beforeSubmit, this);
};
Ext.extend(Kohive.views.videos.New, Kohive.views.pod.New, {
	initListeners: function() {
		//setup the tooltip so it shows when the user rolls over the new field
		this.on('expand', function() {
			var controller = Kohive.OS.getController('videos');
			if (!controller || !controller.indexWindow) return;
			
			this.textField.el.on('mouseover', function() { controller.indexWindow.fireEvent('statusbar-event-start', Ext.ux.Translator.translate('app_videos_field_desc')); }, this);
			this.textField.el.on('mouseout',  function() { controller.indexWindow.fireEvent('statusbar-event-stop'); }, this);
		}, this);
		
		this.controller.on({
		  scope           : this,
		  'video-created' : this.onSuccess
		});
	},
	
  /**
   * Method which is called before the form is submitted
   */
  beforeSubmit: function() {
    this.controller.indexWindow.showSubmitting();
  },
  
  /**
   * Method called when the form is submitted
   */
  onSubmit: function() {
    // Check if the form is valid
    if (!this.getForm().isValid()) return;
    
		this.controller.fireAction('create', null, [this.getForm()]);
    this.doLayout();
  },
  
  onSuccess: function() {
    this.textField.setValue('');
  }
});

/**
 * @class Kohive.views.photos.Video
 * @extends Ext.Panel
 * Displays a single photo in the photos application
 */
Kohive.views.videos.Video = Ext.extend(Ext.Panel, {
  
  initComponent: function() {
    /**
     * @property currentVideo
     * @type Kohive.models.Video
     * The video currently being display (defaults to null until a video is viewed)
     */
    this.currentVideo = null;

    /**
     * @property editableListenersAdded
     * @type Boolean
     * We can only add listeners for the Editable ux once we've rendered an image for the first
     * time, so keep track of whether or not these have been set up here.
     */
    this.editableListenersAdded = false;
    
    /**
     * @property hiveId
     * @type Number
     * A reference to the Hive ID this viewer is for.  This is set to the ID of the current hive when this component
     * is created.  It is required because we only want to listen to events from the photos controller when they are
     * fired for this particular hive.  Each hive has its own instance of the photos application so we only listen
     * to events from the relevant hive.
     */
    this.hiveId = Kohive.OS.getCurrentHive().get('hive_id');
    
    /**
     * @property dataview
     * @type Ext.DataView
     * The DataView used internally to render the image
     */
    this.dataview = new Ext.DataView({
      xtype:        'dataview',
      emptyText:    '',
      autoHeight:   true,
      autoScroll:   true,
			overClass:    'x-over',
      itemSelector: '.x-pod',
      tpl:          this.template,
      listeners: {
        scope       : this,
        'click'     : this.viewOnClick
      }
    });
    
    Ext.applyIf(this, {
      layout: 'fit',
			border: false,
      items:  [this.dataview]
    });
    
    this.controller = Kohive.OS.getController('videos');
    
    Kohive.views.videos.Video.superclass.initComponent.apply(this, arguments);
    
    this.initEvents();
    this.initListeners();
  },
  
  /**
   * Sets up events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event update
       * Relays this view's Ext.ux.Editable update event.
       * @param {Kohive.model.Video} video The video to update
       * @param {Object} updates An object containing fields and values to update
       */
      
      /**
       * @event delete
       * Fired when the user attempts to delete a given video
       * @param {Kohive.models.Video} video The video the user is trying to delete
       */
      'delete'
    );
  },
  
  /**
   * Sets up listeners to events emitted by the photos controller
   */
  initListeners: function() {
    Kohive.OS.getController('videos').on({
      scope:        this,
      'show-video': this.updateVideo,
      'update'    : this.updateVideo
    });
  },
  
  /**
   * Updates this panels' video to the one provided.  Attached to controller's show-video event
   * @param {Kohive.models.Video} video The image to display
   */
  updateVideo: function(video) {
    this.ifCurrentHive(function() {
      if (typeof video == "undefined") return this.hide();

      this.currentVideo = video;

      //create a fake store and set it to the DV's new store
      this.dataview.setStore(new Ext.data.Store({
        autoLoad: true,
        proxy:    new Ext.data.MemoryProxy({"video": [video.data]}),
        reader:   Kohive.models.Video.getReader()
      }));
      
      //remove all listeners and add a custom listener to the update event which will update the main images store
      this.dataview.store.un('update', this.updateOriginalStore, this);
      this.dataview.store.on('update', this.updateOriginalStore, this);

      this.setupEditable();
      this.show();
			
			if (video.get('can_edit')) this.makeEditableFieldsHoverable.defer(20, this);
      // if (video.get('can_delete')) this.makeDeleteButtonHoverable.defer(10, this);
			
      this.doLayout.defer(30, this);
    });
  },
  
  /**
   * 
   */
  updateOriginalStore: function(s, record) {
    var store = this.controller.currentStore;
    store.each(function(r) {
      if (r.get('id') == record.get('id')) {
        r.data = record.data;
        r.set('id', record.get('id'));
        this.controller.fireEvent('videos-loaded', store, this.controller.currentAlbum[Kohive.OS.getCurrentHiveId()]);
      };
    }, this);
  },
  
  makeEditableFieldsHoverable: function() {
    var items  = this.el.select('div'),
        fields = [];

    Ext.each(items.elements || [], function(item) {
      var item = Ext.get(item);

      Ext.each(item.dom.className.split(' '), function(cls) {
        if (cls.split('-')[0] == "action") {
          fields.push(item);
        };
      }, this);
    }, this);

    Ext.each(fields, function(field) {
      field.on('mouseover', field.addClass.createDelegate(field, ['x-over']));
      field.on('mouseout',  field.removeClass.createDelegate(field, ['x-over']));
    }, this);
  },
  
  /**
   * Creates a new Ext.ux.Editable instance bound to the dataview
   */
  setupEditable: function() {
    if (this.editableListenersAdded === true) return;
    
    this.editable = new Ext.ux.Editable(this.dataview, {
      view:  this.controller.indexWindow,
      model: Kohive.models.Video,
      clickActions: [
        {
          name:         'title',
          type:         'edit',
          method:       'dblclick',
          allowBlank:   false
        },
        {
          name:         'description',
          value:        'description',
          type:         'edit',
          fieldType:    'textarea',
					forceRefresh: true,
          method:       'dblclick',
          emptyText:    Ext.ux.Translator.translate('app_add_description')
        },
        {
          name:   'tags',
          type:   'editTags',
          method: 'dblclick'
        }
      ]
    });
    
    this.relayEvents(this.editable, ['update']);
    
    this.editableListenersAdded = true;
  },
  
  /**
   * Makes the new form panel hoverable so the delete button shows/hides
   */
  makeDeleteButtonHoverable: function() {
    var dvEl = this.dataview.getEl(),
				contentEl = dvEl.child('.x-image'),
        toggleEl  = dvEl.child('.x-control-wrap'),
        deleteEl  = dvEl.child('.x-delete-wrap');

		if (!contentEl || !toggleEl) { return; };
    toggleEl.hide().anchorTo(contentEl, 'tl-tl');
		
		if (!deleteEl) { return; };
    deleteEl.hide().anchorTo(contentEl, 'tr-tr', [-10, 0]);
    
    deleteEl.on('click', this.onDeleteClick, this);
    contentEl.on({
      scope: this,
      'mouseout' : function() {
        deleteEl.hide();
      },
      'mouseover': function() {
        deleteEl.show().anchorTo(contentEl, 'tr-tr', [-10, 0]);
      }
    });
  },
	
  /**
   * 
   */
  viewOnClick: function(dataview, index, node, e) {
	  var el     = Ext.get(node),
	      target = Ext.get(e.getTarget()),
	      record = dataview.getRecord(node);
	  
	  //Delete
	  if (target.hasClass('x-delete') || target.parent().hasClass('x-delete')) this.onDeleteClick();
	},
  
  /**
   * Called when the delete button is clicked - displays a confirmation message, fires a 'delete' event
   * if the user confirms
   */
  onDeleteClick: function() {
    return Ext.Msg.show({
      title:   '',
      scope:   this,
      msg:     Ext.ux.Translator.translate('destroy_video_desc', { title: this.currentVideo.get('title')}),
      buttons: {yes:Ext.ux.Translator.translate('ddelete'), no:Ext.ux.Translator.translate('keep')},
      fn:      function(msg) {
        if (msg == 'yes') {
          this.fireEvent('delete', this.currentVideo);
        }
      }   
    });
  },
  
  /**
   * Runs a function if the current hive's ID matches this.hiveId
   * @param {Function} fcn A function to run if the current hive matches
   * @param {Object} scope The scope to execute the function in (defaults to the Viewer scope)
   */
  ifCurrentHive: function(fcn, scope) {
    var currentHiveId = Kohive.OS.getCurrentHive().get('hive_id');
    
    if (currentHiveId == this.hiveId) fcn.call(scope || this);
  },
  
  /**
   * @property template
   * @type Ext.XTemplate
   * The template for the image view
   */
  template: new Ext.XTemplate(
    '<div class="x-pod-wrap">',
      '<tpl for=".">',
        '<div id="video-{id}" class="x-pod x-image x-video<tpl if="can_delete"> x-deletable-hover</tpl><tpl if="can_edit"> x-editable-hover</tpl>">',
          '<div class="x-content-wrap">',
            '<div class="x-control-wrap"><div class="x-control"></div></div>',
            '<div class="x-content">',
							'<tpl if="title">',
	              '<div class="x-title"><div class="x-title action-edit-title">{title}</div></div>',
							'</tpl>',
							'<tpl if="!title">',
								'<tpl if="can_edit">',
		              '<div class="x-title x-empty"><div class="x-title action-edit-title">' + Ext.ux.Translator.translate('app_add_title') + '</div></div>',
		             '</tpl>',
								'<tpl if="!can_edit">',
									'<div class="x-title x-empty"><div class="x-title">&nbsp;</div></div>',
								'</tpl>',
							'</tpl>',
              '<div class="x-extra">',
                '<div class="x-image"><div>{embed_code}</div></div>',
                '<tpl if="description">',
                  '<div class="x-description"><div class="action-edit-description">{description_html}</div></div>',
                '</tpl>',
                '<tpl if="!description && can_edit">',
                  '<div class="x-description x-empty"><div class="action-edit-description">' + Ext.ux.Translator.translate('app_add_description') + '</div></div>',
                '</tpl>',
                
                //Tags
                '<tpl if="tags">',
                  '<div class="x-tags">',
                    '<div class="action-editTags-tags">',
                      '<div class="x-form-field-wrap x-trigger-field-wrap">',
                        '<div class="x-form-text x-superboxselect  x-superboxselect-display-btns">',
                          '<ul>',
                            '<tpl for="tags">',
                              '<li class="x-superboxselect-item no-close">{name}</li>',
                            '</tpl>',
                          '</ul>',
                        '</div>',
                      '</div>',
                    '</div>',
                  '</div>',
                '</tpl>',
                '<tpl if="!tags && can_edit">',
                  '<div class="x-tags x-empty"><div class="action-editTags-tags">' + Ext.ux.Translator.translate('app_add_tags') + '</div></div>',
                '</tpl>',
                
                '<div class="x-other">',
                  //'<div class="x-created-by">' + Ext.ux.Translator.translate('uploaded_by') + ': {author_nickname}</div>',
                  '<div class="x-created-on">' + '{author_nickname}, {created_at_date}</div>',
                  //'<div class="x-created-on">{created_at_date}</div>',
                '</div>',
              '</div>',
              '</div>',
              '<tpl if="can_edit"><div class="x-delete-wrap"><div class="x-delete"><div class="action-delete"></div></div></div></tpl>',
            '</div>',
          '</div>',
        '</div>',
      '</tpl>',
    '</div>'
  )
});

/**
 * @class Kohive.views.boardposts.Index
 * The boardposts index action
 */
Kohive.views.boardposts.Index = Ext.extend(Kohive.views.pod.Index, {
  
  constructor: function(config) {
    var config = config || {};
    
		config.store = Kohive.models.BoardPost.findAll({
	    url: '/memberships/' + Kohive.OS.getCurrentHive().data.id + '/board_posts',
			sortInfo: {
				field:     'created_at',
				direction: 'DESC'
			},
	    listeners: {
	      'load': {
	        scope: this,
	        fn: this.onStoreLoad
	      }
	    }
	  });
		
    Ext.apply(config, {
			url: 'messageboard',
	    hasTags: true,
      columnPanelConfig: {
        items: [
          {field: 'author_nickname',    text: Ext.ux.Translator.translate('sender')},
          {field: 'recipient_nickname', text: Ext.ux.Translator.translate('recipient')},
          {field: 'created_at',         text: Ext.ux.Translator.translate('date')}
        ]
      },
			filterPanelConfig: {
	      filters: [
          {
            ftype:        'store-helper',
            displayField: 'tags.name',
            valueField:   'tags.name',
            title:        Ext.ux.Translator.translate('tags'),
            iconCls:      'tag',
            view:         this,
            listenTo: {
              component: config.store,
              events:    ['load', 'update']
            }
          },
          {
            ftype:        'store-helper',
            displayField: 'author_nickname',
            valueField:   'author_membership_id',
            title:        Ext.ux.Translator.translate('authors'),
            iconCls:      'author',
            view:         this,
            listenTo: {
              component: config.store,
              events:    ['load', 'update']
            }
          },
          {
            ftype:        'store-helper',
            displayField: 'recipient_nickname',
            valueField:   'recipient_membership_id',
            title:        Ext.ux.Translator.translate('recipients'),
            iconCls:      'recipients',
            view:         this,
            listenTo: {
              component: config.store,
              events:    ['load', 'update']
            }
          }
	      ],
	      listeners: {
	        scope            : this,
	        'filters-updated': this.onFiltersUpdated
	      }
	    }
    });
    
    Kohive.views.boardposts.Index.superclass.constructor.call(this, Kohive.models.BoardPost, config);
    
    var memberPanel = Kohive.OS.getMemberPanel();
    if(memberPanel) memberPanel.on('membersloaded', function() { this.store.reload(); }, this);
  },
  
  onStoreLoad: function(store) {
    this.updateFiltersTask = new Ext.util.DelayedTask(this.updateFilters, this);
    this.updateFiltersTask.delay(0);
  }
});

/**
 * @class Kohive.views.boardposts.New
 * @extends Kohive.views.pod.New
 */
Kohive.views.boardposts.New = Ext.extend(Kohive.views.pod.New, {
  
  /**
   * Builds the store for a recipient combo box
   * @return {Ext.data.SimpleStore} The store
   */
  buildMembershipsStore: function() {
    var memberships = [[Ext.ux.Translator.translate('everyone'), null]];
    
    Ext.each(Kohive.OS.getCurrentHiveMemberships(), function(membership) {
      kohive_support_hive_id = 47;
      if (membership.data.hive_id == kohive_support_hive_id && membership.data.access_level > 1) {
        if (membership.get('state') == 'active') {
          if (Kohive.OS.getCurrentHive().get('id') == membership.get('id')) { return; };
        
          memberships.push([membership.get('id_card').full_name, membership.get('id')]);
        };
      } else if (membership.data.hive_id != kohive_support_hive_id) {
        if (membership.get('state') == 'active') {
          if (Kohive.OS.getCurrentHive().get('id') == membership.get('id')) { return; };

          memberships.push([membership.get('id_card').full_name, membership.get('id')]);
        };
      };
    }, this);    
    
    return new Ext.data.SimpleStore({
      fields: ['alias', 'recipient_membership_id'],
      data:   memberships
    });
  },
	
	updateMembershipStore: function() {
		if (!this.assigneeField) return;
		
    var membershipsStore = this.buildMembershipsStore();
		this.assigneeField.store = membershipsStore;
	},
  
  /**
   * Set up the component, create references to textarea and counter
   */
  constructor: function(config) {
    var config = config || {};
    
    membershipsStore = this.buildMembershipsStore();
    
    this.tagsField = new Ext.ux.form.SuperBoxSelect({
      fieldLabel:      Ext.ux.Translator.translate('tags'),
      allowAddNewData: true,
      name:            'tag_list',
      width:           '98%',
      mode:            'local',
      displayField:    'tag',
      valueField:      'tag',
      listeners: {
        scope: this,
        'enter-key': function(bs) {
          this.onCreate();
        },
        'newitem': function(bs, v) {
          var newObj = {
              id:   v,
              name: v,
              tag:  v
          };
          bs.addItem(newObj);
        }
      }
		});
    
    /**
     * @property comboBox
     * @type Ext.form.ComboBox
     * Description
     */
    this.comboBox = new Ext.form.ComboBox({
      fieldLabel:      Ext.ux.Translator.translate('recipient'),
      store:           membershipsStore,
      name:            'recipient_membership_id',
      hiddenName:      'recipient_membership_id',
      triggerAction:   'all',
      mode:            'local',
      displayField:    'alias',
      valueField:      'recipient_membership_id',
      enableKeyEvents: true,
			selectOnFocus:   true,
			forceSelection:  true,
      listeners: {
        'keydown': {
          fn: function(field, e) {
            var key = Ext.EventObject.getKey();
            if (key === 13){
                e.stopPropagation();
            }
          },
          scope: this
        }
      }
    });

    Ext.applyIf(config, {
      defaults: {
        anchor: "-15"
      },
      items: [
        this.comboBox,
        {
          xtype:      'textarea',
          name:       'body',
					allowBlank: false,
					grow:       true,
          id:         'board-posts-body' + Kohive.OS.getCurrentHiveId(),
          fieldLabel: Ext.ux.Translator.translate('message'),
          enableKeyEvents: true,
          listeners: {
            'keydown': {
              fn: function(field, e) {
                var key = Ext.EventObject.getKey();
                if (key === 13) {
                  e.stopPropagation();
									if (Ext.EventObject.ctrlKey) this.onCreate();
                }
              },
              scope: this
            }
          }
        },
				this.tagsField
      ],
			submitButtonConfig: {
				text: Ext.ux.Translator.translate('post')
			}
    });

    Kohive.views.boardposts.New.superclass.constructor.call(this, Kohive.models.BoardPost, config);
		
		// this.on('render', this.comboBox.setValue.createDelegate(this, [null]), this);
		this.on('render', function() {
			this.comboBox.setValue(null);
		}, this);
		
		//update the membership store when the members change
		var memberPanel = Kohive.OS.getMemberPanel();
		memberPanel.on('membersloaded', this.updateMembershipStore, this);
  }
});

// vim: ts=4:sw=4:nu:fdc=2:nospell
/**
 * Ext.ux.HttpProvider extension
 *
 * @author    Ing. Jozef Sakalos
 * @copyright (c) 2008, Ing. Jozef Sakalos
 * @version $Id: Ext.ux.HttpProvider.js 481 2009-01-23 22:10:18Z jozo $
 *
 * @license Ext.ux.HttpProvider is licensed under the terms of
 * the Open Source LGPL 3.0 license.  Commercial use is permitted to the extent
 * that the code/component(s) do NOT become part of another Open Source or Commercially
 * licensed development library or toolkit without explicit permission.
 * 
 * License details: http://www.gnu.org/licenses/lgpl.html
 */

/*global Ext, console */

// {{{
// Define clone function if it is not already defined
if('function' !== Ext.type(Ext.ux.clone)) {
    Ext.ux.clone = function(o) {
        if('object' !== typeof o) {
            return o;
        }
        var c = '[object Array]' === Object.prototype.toString.call(o) ? [] : {};
        var p, v;
        for(p in o) {
            if(o.hasOwnProperty(p)) {
                v = o[p];
                if('object' === typeof v) {
                    c[p] = Ext.ux.clone(v);
                }
                else {
                    c[p] = v;
                }
            }
        }
        return c;
    };
} // eo clone
// }}}

/**
 * @class Ext.ux.HttpProvider
 * @extends Ext.state.Provider
 * @constructor
 * @param {Object} config Configuration object
 */
// {{{
Ext.ux.HttpProvider = function(config) {

    this.addEvents(
        /**
         * @event readsuccess
         * Fires after state has been successfully received from server and restored
         * @param {HttpProvider} this
         */
         'readsuccess'
        /**
         * @event readfailure
         * Fires in the case of an error when attempting to read state from server
         * @param {HttpProvider} this
         */
        ,'readfailure'
        /**
         * @event savesuccess
         * Fires after the state has been successfully saved to server
         * @param {HttpProvider} this
         */
        ,'savesuccess'
        /**
         * @event savefailure
         * Fires in the case of an error when attempting to save state to the server
         * @param {HttpProvider} this
         */
        ,'savefailure'
    );

    // call parent 
    Ext.ux.HttpProvider.superclass.constructor.call(this);

    Ext.apply(this, config, {
        // defaults
         delay:750 // buffer changes for 750 ms
        ,dirty:false
        ,started:false
        ,autoStart:true
        ,autoRead:true
        ,user:'user'
        ,id:1
        ,session:'session'
        ,logFailure:false
        ,logSuccess:false
        ,queue:[]
        ,url:'.'
        ,readUrl:undefined
        ,saveUrl:undefined
        ,method:'post'
        ,saveBaseParams:{}
        ,readBaseParams:{}
        ,paramNames:{
             id:'id'
            ,name:'name'
            ,value:'value'
            ,user:'user'
            ,session:'session'
            ,data:'data'
        }
    }); // eo apply

    if(this.autoRead) {
        this.readState();
    }

    this.dt = new Ext.util.DelayedTask(this.submitState, this);
    if(this.autoStart) {
        this.start();
    }
}; // eo constructor
// }}}

Ext.extend(Ext.ux.HttpProvider, Ext.state.Provider, {

    // localizable texts
     saveSuccessText:'Save Success'
    ,saveFailureText:'Save Failure'
    ,readSuccessText:'Read Success'
    ,readFailureText:'Read Failure'
    ,dataErrorText:'Data Error'

    // {{{
    /**
     * Initializes state from the passed state object or array.
     * This method can be called early during page load having the state Array/Object
     * retrieved from database by server.
     * @param {Array/Object} state State to initialize state manager with
     */
    ,initState:function(state) {
        if(state instanceof Array) {
            Ext.each(state, function(item) {
                this.state[item.name] = this.decodeValue(item[this.paramNames.value]);
            }, this);
        }
        else {
            this.state = state ? state : {};
        }
    } // eo function initState
    // }}}
    // {{{
    /**
     * Sets the passed state variable name to the passed value and queues the change
     * @param {String} name Name of the state variable
     * @param {Mixed} value Value of the state variable
     */
    ,set:function(name, value) {
        if(!name) {
            return;
        }

        this.queueChange(name, value);

    } // eo function set
    // }}}
    // {{{
    /**
     * Starts submitting state changes to server
     */
    ,start:function() {
        this.dt.delay(this.delay);
        this.started = true;
    } // eo function start
    // }}}
    // {{{
    /**
     * Stops submitting state changes
     */
    ,stop:function() {
        this.dt.cancel();
        this.started = false;
    } // eo function stop
    // }}}
    // {{{
    /**
     * private, queues the state change if state has changed
     */
    ,queueChange:function(name, value) {
        var changed = undefined === this.state[name] || this.state[name] !== value;
        var o = {};
        var i;
        var found = false;
        if(changed) {
            o[this.paramNames.name] = name;
            o[this.paramNames.value] = this.encodeValue(value);
            for(i = 0; i < this.queue.length; i++) {
                if(this.queue[i].name === o.name) {
                    this.queue[i] = o;
                    found = true;
                }
            }
            if(false === found) {
                this.queue.push(o);
            }
            this.dirty = true;
        }
        return changed;
    } // eo function bufferChange
    // }}}
    // {{{
    /**
     * private, submits state to server by asynchronous Ajax request
     */
    ,submitState:function() {
        if(!this.dirty) {
            this.dt.delay(this.delay);
            return;
        }
        this.dt.cancel();

        var o = {
             url:this.saveUrl || this.url
            ,method:this.method
            ,scope:this
            ,success:this.onSaveSuccess
            ,failure:this.onSaveFailure
            ,queue:Ext.ux.clone(this.queue)
            ,params:{}
        };

        var params = Ext.apply({}, this.saveBaseParams);
        params[this.paramNames.id] = this.id;
        params[this.paramNames.user] = this.user;
        params[this.paramNames.session] = this.session;
        params[this.paramNames.data] = Ext.encode(o.queue);

        Ext.apply(o.params, params);

        // be optimistic
        this.dirty = false;

        Ext.Ajax.request(o);
    } // eo function submitState
    // }}}
    // {{{
    /**
     * Clears the state variable
     * @param {String} name Name of the variable to clear
     */
    ,clear:function(name) {
        this.set(name, undefined);
    } // eo function clear
    // }}}
    // {{{
    /**
     * private, save success callback
     */
    ,onSaveSuccess:function(response, options) {
        if(this.started) {
            this.start();
        }
        var o = {};
        try {o = Ext.decode(response.responseText);}
        catch(e) {
            if(true === this.logFailure) {
                this.log(this.saveFailureText, e, response);
            }
            this.dirty = true;
            return;
        }
        if(true !== o.success) {
            if(true === this.logFailure) {
                this.log(this.saveFailureText, o, response);
            }
            this.dirty = true;
        }
        else {
            Ext.each(options.queue, function(item) {
                if(!item) {
                    return;
                }
                var name = item[this.paramNames.name];
                var value = this.decodeValue(item[this.paramNames.value]);

                if(undefined === value || null === value) {
                    Ext.ux.HttpProvider.superclass.clear.call(this, name);
                }
                else {
                    // parent sets value and fires event
                    Ext.ux.HttpProvider.superclass.set.call(this, name, value);
                }
            }, this);
            if(false === this.dirty) {
                this.queue = [];
            }
            else {
                var i, j, found;
                for(i = 0; i < options.queue.length; i++) {
                    found = false;
                    for(j = 0; j < this.queue.length; j++) {
                        if(options.queue[i].name === this.queue[j].name) {
                            found = true;
                            break;
                        }
                    }
                    if(true === found && this.encodeValue(options.queue[i].value) === this.encodeValue(this.queue[j].value)) {
                        this.queue.remove(this.queue[j]);
                    }
                }
            }
            if(true === this.logSuccess) {
                this.log(this.saveSuccessText, o, response);
            }
            this.fireEvent('savesuccess', this);
        }
    } // eo function onSaveSuccess
    // }}}
    // {{{
    /**
     * private, save failure callback
     */
    ,onSaveFailure:function(response, options) {
        if(true === this.logFailure) {
            this.log(this.saveFailureText, response);
        }
        if(this.started) {
            this.start();
        }
        this.dirty = true;
        this.fireEvent('savefailure', this);
    } // eo function onSaveFailure
    // }}}
    // {{{
    /**
     * private, read state callback
     */
    ,onReadFailure:function(response, options) {
        if(true === this.logFailure) {
            this.log(this.readFailureText, response);
        }
        this.fireEvent('readfailure', this);

    } // eo function onReadFailure
    // }}}
    // {{{
    /**
     * private, read success callback
     */
    ,onReadSuccess:function(response, options) {
        var o = {}, data;
        try {o = Ext.decode(response.responseText);}
        catch(e) {
            if(true === this.logFailure) {
                this.log(this.readFailureText, e, response);
            }
            return;
        }
        if(true !== o.success) {
            if(true === this.logFailure) {
                this.log(this.readFailureText, o, response);
            }
        }
        else {
            data = o[this.paramNames.data];
            if(!(data instanceof Array) && true === this.logFailure) {
                this.log(this.dataErrorText, data, response);
                return;
            }
            Ext.each(data, function(item) {
                this.state[item[this.paramNames.name]] = this.decodeValue(item[this.paramNames.value]);
            }, this);
            this.queue = [];
            this.dirty = false;
            if(true === this.logSuccess) {
                this.log(this.readSuccessText, data, response);
            }
            this.fireEvent('readsuccess', this);
        }
    } // eo function onReadSuccess
    // }}}
    // {{{
    /**
     * Reads saved state from server by sending asynchronous Ajax request and processing the response
     */
    ,readState:function() {
        var o = {
             url:this.readUrl || this.url
            ,method:this.method
            ,scope:this
            ,success:this.onReadSuccess
            ,failure:this.onReadFailure
            ,params:{}
        };

        var params = Ext.apply({}, this.readBaseParams);
        params[this.paramNames.id] = this.id;
        params[this.paramNames.user] = this.user;
        params[this.paramNames.session] = this.session;

        Ext.apply(o.params, params);
        Ext.Ajax.request(o);
    } // eo function readState
    // }}}
    // {{{
    /**
     * private, logs errors or successes
     */
    ,log:function() {
        if(console) {
            console.log.apply(console, arguments);
        }
    } // eo log
    // }}}

}); // eo extend

// eof

/**
 * This started life as a plugin, moving it here for now as it might be useful on every project (hence oddish definition)
 */


Ext.ns('ExtMVC.plugin.KohiveController');

(function() {
  var c = ExtMVC.plugin.KohiveController;
  
  /**
   * Adds default CRUD (Create, Read, Update, Delete) to this controller.
   * This is currently injected into Controller's prototype, so a simple call to this.actsAsKohiveController()
   * with the following params sets everything up. It adds the following actions:
   * 
   * new() - by default displays the view called 'new' in this controller's view namespace
   * edit() - displays the 'edit' view and loads the model whose id is in your os.params.id
   * create(form) - takes an Ext.form.BasicForm and attempts to create + save a new model object with it
   * update(form) - takes a BasicForm and attempts to update an existing object with it (again using os.params.id)
   * destroy(id, store) - takes an ID, deletes it and optionally refreshes a store
   * 
   * It does not overwrite any existing action already defined under one of those names.
   *
   * Override any of the default methods like this:
   * 
   * this.actsAsKohiveController, MyNamespace.models.MyModel, {
   *   onUpdateFailure: function() {... your custom onUpdateFailure behaviour ...}
   * });
   *
   * @param {ExtMVC.Model} model The model to provide CRUD support for
   * @return {ExtMVC.Controller} The controller, now with addition actions and methods
   */
  c.registerActions = function(model, overrides) {
    Ext.apply(this, overrides, c.defaultFunctions);
    
    this.addEvents(
      /**
       * @event findsuccess
       * Fires after a successful load has taken place (applies to Edit forms)
       * @param {ExtMVC.Model} modelObj The instantiated model object found by the lookup
       */
      'findsuccess',
      
      /**
       * @event findfailure
       * Fires if the model instance could not be found
       */
      'findfailure',

			/**
			 * @event refresh
			 * Fires when the aplciation has been refreshed
			 */
			'refresh'
    );
    
    /**
     * @property model
     * @type ExtMVC.Model
     * Holds a reference to the model this controller provides CRUD support for
     */
    this.model = model;
    
    if (!this.model) {
      throw new Error("You must provide a model to this.actsAsKohiveController().  " +
                      "Pass it as the first argument to actsAsKohiveController or set " + 
                      "'this.model = YourModel' before calling actsAsKohiveController.");
    };
    
    var controller = this;
    
    /**
     * @action index
     * Renders the index window
     */
    this.registerAction('index', function() {
      this.indexWindow = this.renderView('index');
      this.indexWindow.show();

			// TODO this is the crappy work around until we get new mvc
			this.indexWindow.on('update', this.onUpdateEditable, this);
    }, {overwrite: false});
    
    /**
     * @action new 
     * Renders the new form for this model
     */
    this.registerAction('new', function() {
      this.form = this.renderView('new');
    }, {overwrite: false});
    
    /**
     * @action edit 
     * Renders the edit form and loads the model data into it
     */
    this.registerAction('edit', function() {
      this.form = this.renderView('edit');
      // this.form.el.mask('Loading...', 'x-mask-loading');
      
      this.loadForm(this.form);
    }, {overwrite: false});
    
    /**
     * @action create 
     * Creates a new model instance, displays errors if required and redirects to index
     */
    this.registerAction('create', function(form) {
      // form.el.mask('Saving...', 'x-mask-loading');
      this.onCreate(form);
    }, {overwrite: false});
    
    /**
     * @action update
     * Updates an existing model instance, displays errors if required and redirects to index
     */
    this.registerAction('update', function(form) {
      // form.el.mask('Saving...', 'x-mask-loading');
      this.onUpdate(form);
    }, {overwrite: false});
    
    /**
     * @action update-editable
     * Updates an existing model instance, displays errors if required and redirects to index
     */
    this.registerAction('update-editable', function(record, forceRefresh) {
      this.onUpdateEditable(record, forceRefresh);
    }, {overwrite: false});
    
    /**
     * @action destroy 
     * Deletes a given model instance
     */
    this.registerAction('destroy', function(record, store, contentEl) {
      if (record && store && contentEl) {
        var u = new this.model({id: id});
        u.destroy({
          url:     '/memberships/' + Kohive.OS.getCurrentHive().get('id') + '/' + this.model.modelName.pluralize() + '/' + record.get('id'),
          scope:   this,
          success: this.onDestroySuccess.createDelegate(this, [store, contentEl, record]),
          failure: this.onDestroyFailure
        });
      };
    }, {overwrite: false});
    
    /**
     * @action refresh 
     * Calls the onRefresh method
     */
    this.registerAction('refresh', function(shortcut) {
      this.onRefresh(shortcut);
			return true;
    }, {overwrite: false});
  };
  
  c.defaultFunctions = {
    
    /**
     * @property modelObj
     * @type ExtMVC.Model/Null
     * Reference to the model being edited in this form.  Is set once loaded by the adapter
     */
    modelObj: null,
    
    /**
     * @property loadUrl
     * @type String/Null
     * If your form needs to load from a non-standard url, override this (should be very rare).
     * Defaults to null, which lets the model choose which url to load from
     */
    loadUrl: null,
    
    /**
     * @property saveUrl
     * @type String
     * If your form needs to save to a non-standard url, override this (should be very rare).
     * Defaults to null, which lets the model choose which url to save to
     */
    saveUrl: function() {
      return '/memberships/' + Kohive.OS.getCurrentHive().data.id + '/' + this.model.urlName;
    },
  
    /**
     * Loads the form with model data
     * @param {Ext.form.FormPanel} form a reference to the form into which to load the data
     */
    loadForm: function(form) {
      this.model.findById(this.os.params.id, {
        scope:    this,
        url:      this.loadUrl,
        success:  this.onFindSuccess,
        failure:  this.onFindFailure,
        callback: form.el.unmask.createDelegate(form.el)
      });
    },
    
    /**
     * Fires after successful find of the model.  Loads data into the form
     * @param {ExtMVC.Model} modelObj The found model object
     */
    onFindSuccess: function(modelObj) {
      this.editModelObj = modelObj;
      this.form.getForm().loadRecord(modelObj);
      
      this.fireEvent('findsuccess', modelObj);
    },
    
    /**
     * Fires if the model object could not be loaded for whatever reason.
     * By default it offers the user to try again or go back
     */
    onFindFailure: function() {
      this.fireEvent('findfailure');
      Ext.Msg.show({
        title:   'Load Failed',
        msg:     'The item could not be loaded',
        buttons: {yes: 'Try again', no: 'Back'},
        scope:   this,
        fn:      function(btn) { btn == 'yes' ? this.loadForm() : Ext.History.back(); }
      });
    },
    
    /**
     * Called when the save button on an edit form is pressed.  By default this will load the model object with the form values,
     * then call save() on the model object.  Override onCreateSuccess and onCreateFailure to update success and failure callbacks
     * @param {Ext.form.BasicForm} form A reference to the FormPanel
     */
    onCreate: function(form) {
      this.newModelObj = new this.model({});

      this.form = form;

      this.onSave(form, this.newModelObj, {
        success:  this.onCreateSuccess,
        failure:  this.onCreateFailure
      });
    },
    
    /**
     * Called when the save button on an edit form is pressed.  By default this will load the model object with the form values,
     * then call save() on the model object.  Override onUpdateSuccess and onUpdateFailure to update success and failure callbacks
     * @param {Ext.form.BasicForm} form A reference to the FormPanel
     */
    onUpdate: function(form) {
      this.onSave(form, this.editModelObj, {
        success:  this.onUpdateSuccess,
        failure:  this.onUpdateFailure
      });
    },

    /**
     * Called when the save button on an edit form is pressed.  By default this will load the model object with the form values,
     * then call save() on the model object.  Override onUpdateSuccess and onUpdateFailure to update success and failure callbacks
     * @param {Ext.Record} record the record from the model
     * @param {Object} updates The updates to the record
     * @param {Boolean} forceRefresh Parameter description
     */
    onUpdateEditable: function(record, updates, forceRefresh) {
      if (!record) return;
      
      var v = this.indexWindow;
      v.showUpdating();

      /**
       * Updates the model (which also saves it).  Uses success and failure options passed in from
       * one of onUpdate or onCreate, depending on which one called this
       */
      record.update(updates, {
        scope:    this,
        url:      function() { return this.saveUrl() + '/' + record.data.id; },
        success:  this.onUpdateSuccess.createDelegate(this, [forceRefresh]),
        failure:  this.onUpdateFailure
      });
    },
    
    /**
     * Used by both onCreate and onUpdate.  Don't override this unless you understand what you're doing
     */
    onSave: function(form, model, options) {
      var v = this.indexWindow;
      v.showSubmitting();

      // Get the values from the form
      var values = form.getValues();
      
      //chaneg the array into a string
			if (values.tag_list && Ext.isArray(values.tag_list)) values.tag_list = values.tag_list.join(' ');

      // Get the current membership id
      values.author_membership_id = Kohive.OS.getCurrentHive().data.id;

      /**
       * Updates the model (which also saves it).  Uses success and failure options passed in from
       * one of onUpdate or onCreate, depending on which one called this
       */
      model.update(values, Ext.apply({}, options, {
        scope:    this,
        url:      this.saveUrl,
        callback: function() {form.el.unmask();}
      }));
    },
    
    /**
     * Refreshes the the store (if defined) in the index view of the current controller
     */
    onRefresh: function(shortcut) {
      // Get the index view
      var v = this.indexWindow;
      
      // Check if the store exists, and if it does refresh it
      if (v && v.store) {
				if (v.editable.editing) return;
				if (shortcut && shortcut.notificationEl) shortcut.notificationEl.hide();
				
				v.store.load({
          scope   : this,
          callback: function() {
            this.fireEvent('refresh', v.store);
          }
        });
        v.showRefreshing();
      };
    },
    
    /**
     * Called after a successful save.  By default will redirect to the model's index page
     * @param {Object} response The response object from the server
     */
    onSaveSuccess: function(response) {
      // Fire off the refresh action on the controllr
      this.fireAction('refresh');

      //reset the form
      this.form.reset();
			
			//get the first item and focus on it
      var item = this.form.items.items[0];
      if (item && item.focus) {
        var tempTask = new Ext.util.DelayedTask(function() {
          item.focus();
        }, this);
        tempTask.delay(1000);
      };
    },
    
    /**
     * Called after successful item creation.  By default this just first onSaveSuccess
     */
    onCreateSuccess: function() {this.onSaveSuccess();},
    
    /**
     * Called after successful item update.  By default this just first onSaveSuccess
     */
    onUpdateSuccess: function(forceRefresh) {
      var v = this.indexWindow;
      v.showSuccess();
      
      if (forceRefresh) {
        this.fireAction('refresh');
      };
    },
    
    /**
     * Called after save fails on create.  By default this will parse server errors and display them on the form
     * @param {Object} response the response object from the server (should be containing errors)
     */
    onCreateFailure: function(modelObj, response) {
      this.addErrorMessages(modelObj, response);
    },
    
    /**
     * Called after save fails on update.  By default this will parse server errors and display them on the form
     * @param {Object} response the response object from the server (should be containing errors)
     */
    onUpdateFailure: function(modelObj, response) {
      var v = this.indexWindow;
      v.showFailure();
      
      this.addErrorMessages(modelObj, response);
    },
    
    /**
     * Adds server errors to the model and form fields. Private.
     * @ignore
     */
    addErrorMessages: function(modelObj, response) {
      if (!this.form) { return; };
      
      this.form.clearInvalid();
      this.form.markInvalid(modelObj.errors.forForm());
    },
    
    /**
     * Called after an item has been successfully destroyed (deleted).  By default this reloads the grid's store
     * @param {Ext.data.Store} store The Ext.data.Store to reload after deletion
     */
    onDestroySuccess: function(store, contentEl) {
      //check if there is a comments block
      var commentsEl = contentEl.parent().child('.x-comments');
      if (commentsEl) commentsEl.fadeOut({duration: 0.5});
      
      // if (store) store.reload();
			contentEl.fadeOut({
				duration: 0.5,
				scope:    this,
				callback: function() {
				  //check if the store is empty (when you are filtering and delete them all)
				  if (store.totalLength > 1) store.reload();
				  else {
				    var v = this.indexWindow;
            v.onFiltersUpdated({});
				  };
				}
			});
    },
    
    /**
     * Called after an destroy attempt was made on a model instance, but the attempt failed.  By default this shows
     * a MessageBox alert informing the user
     */
    onDestroyFailure: function(paramName) {
      Ext.Msg.alert(
        'Delete Failed',
        Ext.ux.Translator.translate('something_wrong')
      );
    }
  };
  
  /**
   * Define a method on Controller to enable this.actsAsKohiveController(this) within a
   * controller constructor function
   */
  ExtMVC.Controller.prototype.actsAsKohiveController = c.registerActions;
})();

if (typeof Kohive == "undefined") {
  Ext.ns('Kohive');
};

/**
 * @class Kohive.HttpProvider
 * @extends Ext.ux.HttpProvider
 * Extends the HttpProvider so we can use it with our PHP backend.
 */
Kohive.HttpProvider = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    url:        '/states',
    autoRead:   false,
    delay:      750,
    queue:      {},
    sendMethod: 'POST',
    readMethod: 'GET'
  });
  
  Kohive.HttpProvider.superclass.constructor.call(this, config);
};
Ext.extend(Kohive.HttpProvider, Ext.ux.HttpProvider, {
  
  /**
   * Initalizes the Kohive HTTTP state provider by sending off a request to the
   * server and setting this.state
   */
  initState: function() {
    this.state = {};
    
    // Success function
    var successFn = function(response) {
      var o = {};
      
      // Try to decode the response text and log out the error if set
      try {
        o = Ext.decode(response.responseText);
      } catch(e) {
        if (true === this.logFailure) {
          this.log(this.readFailureText, e, response);
        };
        return;
      };
      
      // Loop through each of the items in response
      for (var i = 0; i < o.length; i++) {
        var n = o[i].key;
        var v = o[i].data;
        
        // Decode the value and add it into the state config
        this.state[n] = this.decodeValue(v);
      };
    };
    
    // Failure function
    var failureFn = function(response) {
      if (true === this.logFailure) {
        this.log(this.readFailureText, e, response);
      };
    };
    
    var o = {
      url:     this.readUrl   || this.url,
      method:  this.getMethod || 'GET',
      scope:   this,
      success: successFn,
      failure: failureFn
    };
    
    // Send off the request
    Ext.Ajax.request(o);
  },
  
  // private
  queueChange: function(name, value) {
    var changed = undefined === this.state[name] || this.state[name] !== value;
    var o = {};
    var i;
    var found = false;
    if(changed) {
      var n = name;
      var v = this.encodeValue(value);
      
      if(this.queue[n]) {
        this.queue[n] = v;
        found = true;
      }
      if(false === found) {
        this.queue[name] = this.encodeValue(value);
      }
      this.dirty = true;
    }
    return changed;
  },
  
  // private
  submitState: function() {
    if(!this.dirty) {
      this.dt.delay(this.delay);
      return;
    }
    this.dt.cancel();
    
    var o = {
      url:     this.saveUrl    || this.url,
      method:  this.sendMethod || 'POST',
      scope:   this,
      success: this.onSaveSuccess,
      failure: this.onSaveFailure,
      queue:   Ext.ux.clone(this.queue),
      params:  {}
    };
    
    for (r in o.queue) {
      Ext.apply(o.params, {
        key:  r,
        data: o.queue[r]
      });
    };
    
    // be optimistic
    this.dirty = false;
    
    Ext.Ajax.request(o);
  },
  
  onSaveSuccess: function(response, options) {
    if(this.started) {
      this.start();
    };
    
    this.fireEvent('savesuccess', this);
  },
  
  // private
  readState: function() {
    
    var o = {
      url:     this.readUrl   || this.url,
      method:  this.getMethod || 'GET',
      scope:   this,
      success: this.onReadSuccess,
      failure: this.onReadFailure
    };
    
    Ext.Ajax.request(o);
  },
  
  onReadSuccess: function(response, options) {
    var o = {};
    try {
      o = Ext.decode(response.responseText);
    } catch(e) {
      if (true === this.logFailure) {
        this.log(this.readFailureText, e, response);
      };
      return;
    };
    
    for (item in o) {
      var n = item;
      var v = o[item];
      
      this.state[n] = v;
    };
    
    // this.queue = [];
    this.dirty = false;
    
    if (true === this.logSuccess) {
      this.log(this.readSuccessText, data, response);
    };
    
    this.fireEvent('readsuccess', this);
  }
  
});

/**
 * Kohive.QuickAppWindow
 * @extends Ext.Window
 * Default settings for all Quick App windows
 */
Kohive.QuickAppWindow = function(config) {
  var config = config || {};
 
  Ext.applyIf(config, {
    animateTarget: Kohive.OS.getQuickAppsBar(),
    minimizable:   false,
    resizable:     false,
    draggable:     false,
    constrain:     true,
    hidden:        true,
    shadow:        false,
    cls:           'x-quick-app-window',
    closable:      false,
    height:        300,
    width:         300
  });
 
  Kohive.QuickAppWindow.superclass.constructor.call(this, config);
  
  /**
   * @property hiding
   * @type Boolean
   * Keeps track of whether or not this window is currently in the middle of a hide animation
   */
  this.hiding = false;
  
  //align the window next to its calling element, slide in rather than just show
  this.on('show', function() {
    Ext.getBody().on('mousedown', this.clickListener, this);
    
    this.setActive();
    this.el
      .anchorTo(Kohive.OS.getQuickAppsBar().el, 'tr-tl', [-5, 5])
      .slideIn('tr', { duration: 0.2, easing: 'easeBoth' })
      .fadeIn({ duration: 0.2, concurrent: true });
  }, this);

  //start hide animation and return false.  Hide event is called again on animation completion
  this.on('beforehide', function() {
    if (this.hiding === false) {
      this.hiding = true;
      
      this.el.slideOut('tr', {
        duration: 0.2,
        easing:   'easeBoth',
        scope:    this,
        callback: function() {
          this.hide();
        }
      }).fadeOut({ duration: 0.2, remove: false, concurrent: true});
      return false;
    };
    
  }, this);
  
  this.on('hide', function() { this.hiding = false; Ext.getBody().un('mousedown', this.clickListener, this);}, this);
};
Ext.extend(Kohive.QuickAppWindow, Ext.Window, {
  /**
   * Listens to clicks and hides the panel if the click was not inside the panel
   * This is intended to be attached to Ext.getBody().
   * @param {Ext.EventObject} event The click event
   */
  clickListener: function(event) {
    if (!event.getTarget('.x-quick-app-window') && !event.getTarget('.x-shortcut-quick-app')) {
      this.close();
    };
  }
});
Ext.reg('quick_app_window', Kohive.QuickAppWindow); 

Ext.ns('Ext.ux.form');

/**
 * Ext.ux.form.SearchField
 * @extends Ext.form.TriggerField
 * Basic search field for use in the Header Toolbars in each application
 */
Ext.ux.form.SearchField = function(config) {
  var config = config || {};
 
  // Ext.applyIf(config, {
  //   triggerClass: 'x-form-searchfield-trigger',
  //   
  //   //TODO: implement this.....
  //   onTriggerClick: function() {
  //     alert('clicked the trigger');
  //   }
  // });
	
	Ext.applyIf(config, {
		cls:             'x-search-field',
		emptyClass:      'x-empty',
	  enableKeyEvents: true
	});
 
  Ext.ux.form.SearchField.superclass.constructor.call(this, config);
  
  this.on('render', this.addWrapper, this);
};

Ext.extend(Ext.ux.form.SearchField, Ext.form.TextField, {
  /**
   * Add a wrapper to the field so we can style it better
   */
  addWrapper: function() {
    var wrapper = this.el.insertSibling({
      cls: 'x-search-field-wrapper'
    });
    
    wrapper.appendChild(this.el);
  }
});
Ext.reg('searchfield', Ext.ux.form.SearchField); 

/**
 * Ext.ux.Shortcut
 * @extends Ext.Component
 * @author Ed Spencer
 * Generic shortcut implementation
 * You would usually not use this directly, instead use DesktopShortcut, QuickLaunchShortcut and StartMenuShortcut or define your own specialised subclass.  Remember to provide your own template when subclassing otherwise you will end up with empty divs instead of anything useful.  Check out DesktopShortcut for a simple example of this.
 * 
 */
Ext.ux.Shortcut = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    text: 'Some Desktop Shortcut',
    id:    "shortcut-" + config.position
  });
  
  this.config = config;
  this.id = config.id;
  
  Ext.ux.Shortcut.superclass.constructor.call(this, config);  
};

Ext.extend(Ext.ux.Shortcut, Ext.Component, {
  hoverClass:    'x-shortcut-hover',
  selectedClass: 'x-shortcut-selected',
  
  /**
   * @config {Ext.Template} template The template to use when rendering this component's HTML.  The defaults to an empty div so you will need to override it
   */
  template: new Ext.Template('<div></div>'),
    
  /**
   * Renders the shortcut using its template, sets up various listeners
   */
  onRender: function(container) {
    this.el = this.template.append(container, [this.config.text, this.config.icon, this.id], true);
    this.el.on('mousedown',   this.onMouseDown,   this);
    this.el.on('dblclick',    this.onDblClick,    this);
    this.el.on('contextmenu', this.onContextMenu, this);
    this.el.on('mouseover',   this.onHover,       this);
    this.el.on('mouseout',    this.onHoverOut,    this);
  },
  
  onDblClick: function() {
    ExtMVC.NotificationManager.inform('Desktop item double clicked');
  },
  
  onHover: function() {
    this.el.addClass(this.hoverClass);
  },
  
  onHoverOut: function(e) {
    this.el.removeClass(this.hoverClass);
  },
  
  /**
   * Marks the shortcut as selected if the mousedown event is from a left click
   */
  onMouseDown: function(e) {
    if (e.button == 0) {
      this.select();
    };
  },
  
  /**
   * Marks this shortcut as selected (unselects all other shortcuts)
   */
  select: function() {
    Ext.ux.Shortcut.unselectAllShortcuts();
    this.el.addClass(this.selectedClass);
  },
  
  /**
   * Called on contextmenu event.  Override with your own logic
   * @param {Ext.EventObject} e The event object
   */
  onContextMenu: Ext.emptyFn,
  
  /**
   * Unselects this shortcut
   */
  unselect: function() {
    this.el.removeClass(this.selectedClass);
  }
});

/**
 * Removes this.selectedClass from all shortcuts
 * @param {String} xtype The xtype to find and unselect instances of.  Defaults to 'shortcut', change to one of the other shortcut xtypes (e.g. 'desktop_shortcut') to target just that type
 */
Ext.ux.Shortcut.unselectAllShortcuts = function(xtype) {
  var xtype = xtype || 'shortcut';
  var xtypeRegExp = new RegExp(xtype);
  
  var shortcuts = Ext.ComponentMgr.all.filterBy(function(e) {return xtypeRegExp.test(e.getXTypes()); });
  Ext.each(shortcuts, function(e) {if (e) {e.unselect();}});
};

Ext.reg('shortcut', Ext.ux.Shortcut);



/**
 * Ext.ux.DesktopShortcut
 * @extends Ext.ux.Shortcut
 * Provides a desktop icon component with right click menu
 */
Ext.ux.DesktopShortcut = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    contextMenuConfig: {
      items: [
        {
          text:    'Delete this shortcut',
          scope:   this,
          iconCls: 'delete',
          handler: function() {
            ExtMVC.NotificationManager.inform('Deleting shortcut ' + this.id);
          }
        }
      ]
    }
  });
  
  Ext.ux.DesktopShortcut.superclass.constructor.call(this, config);
};

Ext.extend(Ext.ux.DesktopShortcut, Ext.ux.Shortcut, {
  template: new Ext.Template(
    '<div class="x-shortcut x-shortcut-desktop" id="{2}">',
      '<div class="x-shortcut-icon"><img src="{1}" /></div>',
      '<span unselectable="on">{0}</span>',
    '</div>'
  ),
  
  /**
   * Intercepts right click and displays a context menu.  Override to provide your own implementation
   */
  onContextMenu: function(e) {
    e.stopEvent();
    this.select();
    
    if (!this.contextMenu) {
      this.contextMenu = new Ext.menu.Menu(this.config.contextMenuConfig);
    };
    
    //make sure the context menu has been rendered...
    if (!this.contextMenu.el) { this.contextMenu.render();}
    
    this.contextMenu.showAt(e.getXY());
  }
});
Ext.reg('desktop_shortcut', Ext.ux.DesktopShortcut);

/**
 * Ext.ux.KohiveShortcut
 * @extends Ext.ux.Shortcut
 * Base class for Application Launcher and Quick App shortcuts
 */
Ext.ux.KohiveShortcut = function(config) {
  var config = config || {};
  Ext.applyIf(config, {
    text: "Application Shortcut",
    id:   Ext.id()
  });

  /**
   * @property shortcutTip
   * @type Kohive.ShortcutTip
   * Maintains a reference to a shortcut tip for this shortcut
   */
  this.shortcutTip = null;
	
	/**
	 * @property application_hive_memberships_id
	 * @type Integer
	 * The application_hive_memberships_id
	 */
	this.application_hive_memberships_id = config.application_hive_memberships_id;
  
  Ext.ux.KohiveShortcut.superclass.constructor.apply(this, arguments);
  
	/**
	 * Try and find the controller using the config.controller config
	 */
	if (config.controller) this.controller = Kohive.OS.getController(config.controller);
	else this.controller = null;
	
	this.initListeners();
	
  // Check if the application is starred
  if (config.starred && config.application_hive_memberships_id) {
    this.notification = true;
  } else {
    this.notification = false;
  };
};

Ext.extend(Ext.ux.KohiveShortcut, Ext.ux.Shortcut, {
  onDblClick: Ext.emptyFn,

	initListeners: function() {
		this.on('render', function() {
			if (this.controller) this.controller.on('refresh', this.removeStarred, this);
      this.el.on('click', this.onClickEvent, this);
    }, this);
	},
  
	onClickEvent: function() {
		this.removeStarred(this.initialConfig.application_hive_memberships_id);
		
		// Fire off an event on the controller to refresh any data
		if (this.controller) this.controller.fireAction('refresh', this.controller);
	},
	
  /**
   * Sends off a request to the server to remove the unread status of the application
   * @param {Integer} application_hive_memberships_id The id of the application_hive
   */
  removeStarred: function() {
		if (this.initialConfig.starred && this.initialConfig.application_hive_memberships_id) {
	    var model = new Kohive.models.ApplicationHiveMembership({
	      id:      this.initialConfig.application_hive_memberships_id,
	      starred: false
	    });
    
	    model.save();
		
			this.hideNotification();
			
			if (this.controller) this.controller.un('refresh', this.removeStarred, this);
		};
  },
  
  /**
   * Displays this shortcuts ShortcutTip
   */
  showShortcutTip: function() {
    if (this.bubble) this.bubble.hide();
    
    if (this.shortcutTip) {
      if (this.shortcutTip.hidden) {this.shortcutTip.show();}
    } else {
      var shortcut = this.el;
      this.shortcutTip = new Kohive.ShortcutTip({
        text:      this.initialConfig.text,
        app:       this.initialConfig,
        listeners: {
          render: function(c) { c.show(); c.addOptions(); },
          show:   function(c) { c.el.anchorTo(shortcut, 'l-r', [0, -16]).show(); },
          hide:   function(c) { c.el.fadeOut({duration: 0.15}); } //FIXME: doesn't get used
        }
      });
    }
  },
  
  /**
   * Hides this Shortcut's ShortcutTip
   */
  hideShortcutTip: function() {
    if (this.shortcutTip && !this.shortcutTip.hidden) { this.shortcutTip.hide(); if (this.bubble) { this.bubble.hide(); }; }
  },
  
  /**
   * Adds mouseover and mouseout events to show and hide this shortcut's ShortcutTip
   */
  onRender: function() {
    Ext.ux.KohiveShortcut.superclass.onRender.apply(this, arguments);
    this.el.on('click', this.onClick, this);
    
    // Check if this shortcut is inside the application launcher
    // by checking what it's xtype is.
    if (this.xtype == 'application_launcher_shortcut') {
      // Create the notification element
      this.notificationEl = this.el.createChild({
        tag: 'a',
        cls:  'x-notification'
      });
      this.notificationEl.enableDisplayMode();
			
      // Check if the icon needs to be displayed on load
      if (!this.notification) this.notificationEl.hide();
			else this.refreshControllerIfOpen();
    }
  },
	
	/**
	 * Function which is called when a app gets an update (every 15 secs i think)
	 * if it has an update then this method is called to check if the app view is open, if it is
	 * it fires the refresh action in the app controller
	 */
	refreshControllerIfOpen: function() {
		// Fire off an event on the controller to refresh any data
		if (!this.initialConfig.controller) return;
		
		var controller = Kohive.OS.getController(this.initialConfig.controller);
		if (!controller) return;
		
		var v = controller.runningViews[this.initialConfig.controller + '_index_' + Kohive.OS.getCurrentHiveId()];
		
		if (v && v.isVisible()) {
			controller.fireAction('refresh', controller, [this]);
		};
	},
  
  /**
   * Tells the Application Launcher to launch the application
   */
  onClick: function(e) {
    var an = this.initialConfig.text;
    if (an == "Overview" || an == "Passwords" || an == "Private Messages") {
      this.bubble = new Ext.ux.HUD({
        anchorToEl: this.el,
        html: Ext.ux.Translator.translate('coming_soon'),
        header: false,
        width: 'auto',
        cls: 'coming-soon',
        clickListener: function(e) {
          if (!e.getTarget('.x-hud-wrapper')) {
            this.destroy();
          };
        }
      });
      this.bubble.render();
      
      this.hideShortcutTip();
      
      this.bubble.hide();
      
      var bubble = this.bubble;
      this.bubble.showHUD([25, 0], 'l-r', 1, function() {
        bubble.positionPoker([0, 2], 'r-l');
        
        var tempTask = new Ext.util.DelayedTask(function() {
          bubble.el.fadeOut();
        }, this);
        tempTask.delay(1500);
      });
      
      // bubble.el.anchorTo(this.el, 'r-t', [-50, 0]);
    } else if(an == "Feedback") {
			feedback_widget.show();
    } else {
			// Fix for chat/chatroom
			if (this.initialConfig.controller == "chatroom") this.initialConfig.controller = "chat";
			if (this.initialConfig.controller == "blog") this.initialConfig.controller = "blogs";
			
			var controller = Kohive.OS.getController(this.initialConfig.controller);
			if (controller) controller.fireAction('refresh');
			
      Kohive.OS.dispatch(this.initialConfig);
    };
  },
  
  /**
   * Shows the notification icon on the hive button. Defaults to false.
   */
  showNotification: function(){
    if (!this.notification) {
      this.notificationEl.fadeIn({ duration: 0.5 });
      this.notification = true;
    };
  },
  
  /**
   * Hides the notification icon on the hive button.
   */
  hideNotification: function() {
    if (this.notification) {
      this.notificationEl.fadeOut({ duration: 0.5 });
      this.notification = false;
    };
  },
  
  /**
   * Toggles the notification icon on the hive button.
   * @see #showNotification
   * @see #hideNotification
   */
  toggleNotification: function(){
    if (this.notification) {
      this.hideNotification();
    } else {
      this.showNotification();
    };
  }
});
Ext.reg('kohive_shortcut', Ext.ux.KohiveShortcut); 

/**
 * Ext.ux.ApplicationLauncherShortcut
 * @extends Ext.ux.KohiveShortcut
 * Shortcut config for use on the Kohive Application Launcher bar
 */
Ext.ux.ApplicationLauncherShortcut = Ext.extend(Ext.ux.KohiveShortcut, {
  template: new Ext.Template(
    '<div class="x-shortcut-application-launcher {2}">',
      '<div class="x-shortcut-icon"><img src="{1}" id="app-launcher-shortcut-{2}-image" /></div>',
    '</div>'
  ),
  
  /**
   * Sets up events on this shortcut
   * @param {Object} config Configuration object
   */
  constructor: function(config) {
    Ext.ux.ApplicationLauncherShortcut.superclass.constructor.apply(this, arguments);
  },
  
  /**
   * Adds drag functionality to the member button
   */
  initalizeDragSource: function(){
    var member = this;
    
    // Check if the currently logged in user has permission to delete applications (must be a > mod)
    if (!Kohive.OS.hasPermission()) { return; };
    
    this.el.dd = new Ext.dd.DragSource(this.el, {
      
      /**
       * @property ddGroup
       * The drag group the dragsource is part of
       */
      ddGroup:        'memberPanel',
      
      /**
       * @property member
       * Instance of Kohive.models.Membership
       */
      member:         this.member,
      
      /**
       * Called when the user starts dragging a member
       * @param {Ext.Event} e Event
       */
      onDrag: function(e) {
        var dragEl = Ext.get(this.getDragEl());
        var el = Ext.get(this.getEl());
        // var innerEl = el.child('.x-member-avatar');
        
        if (el.dom.innerHTML != dragEl.dom.innerHTML) {
          dragEl.applyStyles({'z-index':20000});
          dragEl.update(el.dom.innerHTML);
        
          dragEl.addClass('x-dragger');
          dragEl.addClass(el.dom.className);
        };
      },
      
      onDrop: function(e) {
        
      },
    
      /**
       * Changes the default x+y offset when dragging a member
       * @param {String} x current x position 
       * @param {String} y current y position 
       */
      autoOffset: function(x, y) {
        var memberXY = member.el.getXY();
      
        xDelta = x - memberXY[0];
        yDelta = y - memberXY[1];
      
        this.setDelta(xDelta, yDelta);
      },
    
      /**
       * Called when the member has not been dropped on anything
       */
      afterRepair: function() {
        this.dragging = false;
      }
    });
  }
});
Ext.reg('application_launcher_shortcut', Ext.ux.ApplicationLauncherShortcut);

/**
 * Ext.ux.QuickAppShortcut
 * @extends Ext.ux.KohiveShortcut
 * Shortcut config for use on the Kohive Quick Apps bar
 */
Ext.ux.QuickAppShortcut = Ext.extend(Ext.ux.KohiveShortcut, { 
  template: new Ext.Template(
    '<div class="x-shortcut x-shortcut-quick-app" id="quick-app-shortcut-{2}">',
      '<div class="x-shortcut-icon"><img src="{1}" /></div>',
    '</div>'
  )
});
Ext.reg('quick_app_shortcut', Ext.ux.QuickAppShortcut);

/**
 * Kohive.ShortcutTip
 * @extends Ext.Component
 * Provides a custom tooltip for any Kohive shortcut
 */
Kohive.ShortcutTip = function(config) {
  var config = config || {};
 
  Ext.applyIf(config, {
    renderTo:  Ext.getBody(),
    autoWidth: true
  });
  
  Kohive.ShortcutTip.superclass.constructor.call(this, config);
  
  this.addEvents(
    /**
     * @event delete
     * Fires when the delete button is clicked
     * @param {Kohive.ShortcutTip} this The ShortcutTip object containing the delete button
     */
    'delete'
  );
  
  this.on('render', this.addOptions, this);
  this.el.hover(Ext.emptyFn, this.hideUnlessHovering, this);
};
Ext.extend(Kohive.ShortcutTip, Ext.Component, {
  /**
   * @property template
   * @type Ext.Template
   * Default template for any Shortcut Tooltip
   */
  template: new Ext.Template(
    '<div class="kohive-shortcut-tip-wrapper">',
      '<div class="kohive-shortcut-tip-center">',
        '<span class="kohive-shortcut-tip-text">{text}</span>',
        '<span class="kohive-shortcut-tip-end">',
      '</div>',
    '</div>'
  ),
  
  /**
   * Hides this shortcut tip unless hovering over the application launcher
   * @param {Ext.EventObject} event The hover out event
   */
  hideUnlessHovering: function(event) {
    try {
      if (!Ext.get(event.getRelatedTarget()).findParent('.x-application-launcher-wrapper')) {
        this.hide();
      };
    } catch(e) {this.hide();}
  },
  
  /**
   * Creates the containing element
   */
  render: function(container, position) {
    this.el = container.createChild({
      tag:   'div',
      cls:   'kohive-shortcut-tip'
    });
    
    this.template.append(this.el, this.initialConfig);
    
    Kohive.ShortcutTip.superclass.render.apply(this, arguments);
  },
  
  addOptions: function(config){
    var config = config || {};
    
    // TODO implement delete + click + drag
    
    // var optionsWrapper = this.el.child('.kohive-shortcut-tip-center');
    // this.optionsEl = optionsWrapper.createChild({
    //   tag:  'span',
    //   cls:  'kohive-shortcut-tip-options'
    // });
    // 
    // this.optionsEl.on('click', function() {
    //   // Get the OS and the controller
    //   // var os         = ExtMVC.OS.getOS();
    //   // var controller = os.getController('apps');
    //   
    //   // console.log(this.app);
    //   // 
    //   // return;
    //   
    //   var os = Kohive.OS;
    //   var controller = os.getController('apps');
    //   controller.fireAction('destroy', this, [this.app]);
    // }, this);
  }
});

Ext.ns('Ext.ux.layout');

/**
 * Ext.ux.layout.SlidingCardLayout
 * @extends Ext.layout.CardLayout
 * Same as a Card layout, but animates the switching of panels
 */
Ext.ux.layout.SlidingCardLayout = Ext.extend(Ext.layout.CardLayout, {
  
  setActiveItem: function(id) {
    Ext.ux.layout.SlidingCardLayout.superclass.setActiveItem.apply(this, arguments);
    
    //causes the card to be activated so that we can perform any data loads etc
    var item = this.container.getComponent(id);
    if (item) {
      item.fireEvent('activate', item);
    };
    
    /**
     * Hmm... no good so far, reverting to basic Card layout as above
     */
    
    // console.log('setting active: ' + id);
    
    var item = this.container.getComponent(id);
    if(this.activeItem != item) {
      
      if (this.activeItem) {
        this.activeItem.el.slideOut('t', {
          scope:    this,
          callback: function(e) {
            // e.hide();
            
            // console.log(e);
            // console.log(item);
            
            this.activeItem = item;
            item.show();
            item.el.slideIn('b');
            this.layout();
          }
        });  
      } else {
        this.activeItem = item;
        this.activeItem.show();
      };

    }    
  }
});

Ext.Container.LAYOUTS['sliding_card'] = Ext.ux.layout.SlidingCardLayout;

/**
 * Kohive.Panel
 * Abstract base class for all Kohive Widgets - subclass this to create your own widgets
 */
Kohive.Widget = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    baseCls:  'x-widget',
    height:   90
  });
  
  Kohive.Widget.superclass.constructor.call(this, config);
};

Ext.extend(Kohive.Widget, Ext.Component, {
  /**
   * @property templateConfig
   * @type Object
   * Configuration values to be passed into the template
   */
  templateConfig: {},
  
  /**
   * Overwrite default template for the widget
   */
  tpl: new Ext.Template(''),
  
  /**
   * Shows the preferences side of the widget.
   */
  showPreferences: function(){
    
  },
  
  // private
  render: function(ct, position){
    var ct = ct || Ext.getBody();
    
    // Create the element
    this.el = ct.createChild({
      cls: this.baseCls
    });
    
    // Add the class to the element if defined
    if(this.cls){
      this.el.addClass(this.cls);
    }
    
    // Update the element with the defined template
    if(this.tpl && this.el){      
      this.tpl.overwrite(this.el, this.templateConfig);
    }
    
    Kohive.Widget.superclass.render.apply(this, arguments);
  } 
});

Ext.reg('kohive_widget', Kohive.Widget);

/**
 * @class Ext.ux.Editable
 * @extends Ext.util.Observable
 * @title Ext.ux.Editable
 * @author rdougan (Robert Dougan) <rdougan@me.com>
 * @description Allows for inline edititing for fields. Currently supports textfields, textareas,
 *              clicking methods, tags and custom methods through a MVC Model.
 *
 *              You can add this functionality to your view by calling this after your super:
 *                Ext.applyIf(this, Ext.ux.Editable);
 * 
 *              To allow editing to work, your view must have an array (this.clickActions) which contains:
 *                {String} name The name of the field you want to edit, must be a field in your model
 *                {String} type The type of field (edit, click, custom)
 *                {String} method The event method you want it initalized with (click, dblclick, mouseover etc)
 *              
 *              Other options which are not neccessary are:    
 *                {String} fieldName The field you want to edit from your model if it is not the same as the specified name.
 *                {String} fieldType The fieldtype you want to use. (text, textarea). Defaults to text.
 *                {String} value The field (from your model) that you want to use for the fields valid. Useful for tags.
 *                {Boolean} forceHeight Whether to set the height of the textarea to the height of the editable div. Defaults to false.
 *                {Boolean} forceRefresh Whether to refresh the dataviews store on save. Defaults to false.
 *                {Boolean} allowBlank A simple validation whether it can be blank or not. Defaults to false.
 * 								{Number} maxLength If set it checks if the valud is longer than what is set
 * 								{Ext.form.VTypes} vtype If set, it validates the new valid witht he vtype
 */
Ext.ux.Editable = Ext.extend(Ext.util.Observable, {
	
  /**
   * @constructor
   * @param {Ext.DataView} dataview The element to make editable
   * @param {Object} config Configuration options
   * @cfg {editableEvents} Array An array of events on the Element to react to (defaults to ['click', 'dblclick'])
   * @cfg {Array} clickActions A user-defined array of fields which can be made editable
   */
  constructor: function(dataview, config) {
    config = config || {};
    
    Ext.applyIf(config, {
      editableEvents: ['click', 'dblclick']
    });
    
    Ext.apply(this, config, {
      /**
       * @property usePermissions
       * @type Boolean
       * Whether to check if the user can edit each record by checking the has_permission field in the record
       */
      usePermissions: true,
      
      /**
       * @property dataview
       * @type Ext.DataView
       * The DataView bound to this Editable instance (set by the constructor)
       */
      dataview: dataview,
      
      /**
       * @property clickActions
       * @type Array
       * The user-defined clickActions config array
       */
      clickActions: []
    });
    
    Ext.ux.Editable.superclass.constructor.apply(this, arguments);
    
    Ext.each(this.editableEvents, function(eventName) {
      this.dataview.on(eventName, this.delegateClick, this);
    }, this);

		this.dataview.store.on('load', this.initEditableMouseover, this);
    
    this.initEvents();
  },
  
  /**
   * Sets up the events emitted by this component
   */
  initEvents: function() {
    this.addEvents(
      /**
       * @event update
       * Fired when the user has updated a field
       * @param {ExtMVC.Model.Base} model The model instance
       * @param {Object} updates An object containing a key: value pair for the field to update with a new value
       */
      'update'
    );
  },
  
  /**
   * Listens to clicks inside the DataView's element and dispatches appropriately.
   * This is intended to be attached to one of both of the click and dblclick events
   * @param {Ext.DataView} dataview The Data view firing the click event
   * @param {Integer} index The index of the clicked record
   * @param {Ext.Element} node The HTML node of the clicked record
   * @param {Ext.Eevent} e The event object
   */
  delegateClick: function(dataview, index, node, e) {
		var el, record;
		
		// Hack for gridPanel, rowclick + rowdblclick do not have a 'node' param
		if (!e) {
			e      = node;
			el     = Ext.get(e.getTarget());
			record = dataview.getStore().getAt(index);
		} else {
			el     = Ext.get(e.getTarget());
	    record = dataview.getRecord(node);
		};
		
    Ext.each(this.clickActions, function(action) {
      var a = {};
      Ext.apply(a, action, {
        css:       action.name,
        type:      action.type      || '',
        method:    action.method    || '',
        field:     action.field     || action.name,
        fieldType: action.fieldType || 'text',
				maxLength: action.maxLength || Number.MAX_VALUE
      });
      
      // Generate the cls which is used for the editable field
      a.cls = 'action-' + action.type + '-' + a.css;
      
      if (a.needsPermission !== false) a.needsPermission = true;
			
      //FIXME: naughty hack 1 of 2 - sometimes editable fields contain html.  This just runs the comment out code below once
      //for the element itself and once for the parent.  Dirty :/
      if      (el && el.hasClass(a.cls))                                                                         this.tempFunction(el, a, record, dataview, index, e);
      else if (el.parent() && el.parent().hasClass(a.cls))                                                       this.tempFunction(el.parent(), a, record, dataview, index, e);
      else if (el.parent().parent() && el.parent().parent().hasClass(a.cls))                                     this.tempFunction(el.parent().parent(), a, record, dataview, index, e);
      else if (el.parent().parent().parent() && el.parent().parent().parent().hasClass(a.cls))                   this.tempFunction(el.parent().parent().parent(), a, record, dataview, index, e);
      else if (el.parent().parent().parent().parent() && el.parent().parent().parent().parent().hasClass(a.cls)) this.tempFunction(el.parent().parent().parent(), a, record, dataview, index, e);

    //   // Check if the target element has the correct class
    //   if (el.hasClass(a.cls) || el.parent().hasClass(a.cls)) {
    //     // Check if the correct type of event has been fired for the clickAction (click, dblclick etc)
    //     if (typeof action.method != 'undefined' && action.method !== e.type) return;
    //     
    //     // Check if the user has permission to edit the field
    //     var hasPermission;
    //     if (action.needsPermission === false) {
    //       hasPermission = true;
    //     } else if (action.needsPermission === true || this.usePermissions) {
    //       if (record.data.has_permission) { hasPermission = true; } else { hasPermission = (this.pods) ? this.pods[index].hasPermission : Kohive.OS.hasPermission(record); };
    //     } else {
    //       hasPermission = true;
    //     };
    //     
    //     // Check if the type of action is edit, and start the field edit process
    //     if (action.type == "edit" && hasPermission)  { return this.beginFieldEdit(dataview, record, el, a, index); };
    //     
    //     // Check if the type of action is click, and start the click action
    //     if (action.type == "click") { return this.doClickAction(dataview, record, el, a, index); };
    //     
    //     // Check if the type is set in the model it is custom
    //     if (this.model && this.model.prototype[action.type] && hasPermission) {
    //       return this.model.prototype[action.type](dataview, record, el, a, index, this);
    //     };
    //   };
    }, this);
  },
  
  // Naughty hack part 2 of 2 - see above
  tempFunction: function(el, a, record, dataview, index, e) {
		// Check if the field is empty or not
		var isEmpty = el.hasClass('x-empty') || el.parent().hasClass('x-empty') || el.hasClass('empty') || el.parent().hasClass('empty');
		if (isEmpty) a.method = e.type;
		
    // Check if the correct type of event has been fired for the clickAction (click, dblclick etc)
    if (typeof a.method != 'undefined' && a.method !== e.type) return;
    
    // Check if the type of action is edit, and start the field edit process
    if (a.type == "edit" && record.get('can_edit'))  { return this.beginFieldEdit.defer(200, this, [dataview, record, el, a, index]); };
		
    // Check if the type of action is click, and start the click action
    if (a.type == "click") { return this.doClickAction(dataview, record, el, a, index); };
		
    // Check if the type is set in the model it is custom
    if (this.model && this.model.prototype[a.type]) {
			//check if it needs permission
			if (a.needsPermission === true && !record.get('can_edit')) return;
			
      return this.model.prototype[a.type](dataview, record, el, a, index, this.view, this);
    };
  },
  
  /**
   * TODO change this to a openWindowAction or something as it is currently too generic
   * 
   * Currently opens a new window/tab with the specified value from the record/clickAction.
   * @param {Ext.DataView} dataView The dataview component
   * @param {Ext.Record} record The recorded from the clicked pod
   * @param {Ext.Element} el The el which was clicked
   * @param {Object} action The action which has to be completed
   */
  doClickAction: function(dataView, record, el, action) {
    var location = record.data[action.fieldName] || null;
    
    // Check if there is a location
    if (!location) { return; };
    
    // Open new window with location
    window.open(location);
  },
  
  /**
   * Hides the element in the DV template and inserts a editable field
   * @param {Ext.DataView} dataView The dataview component
   * @param {Ext.Record} record The recorded from the clicked pod
   * @param {Ext.Element} el The el which was clicked
   * @param {Object} action The action which has to be completed
   * @param {Integer} index The index of the record
   */
  beginFieldEdit: function(dataView, record, el, action, index) {
    // Check if something else is currently being edited
    if (this.editing) return;
    
    // Set the editing property to true
    this.editing = true;
    
    // Get the value from the field if a value hasnt been set for the action
    var value = "";
    if (action.value) {
      action.value = record.data[action.value];
      
      if (typeof action.value == "object") {
        var array = action.value;
        
        Ext.each(array, function(item, index) {
          if (index == array.length - 1) { return value = value + item.name; };
          value = value + item.name + ' '; // use space as delimiter/separator
        }, this);
      } else {
        value = action.value || "";
      };
    } else {
      value = el.dom.innerHTML;
    };
    
    action.forceHeight = true;
    
    // Get the height of the elements height if the forceHeight is on
    if (action.forceHeight) { var elHeight = el.getHeight(); };
    
    // Hide the node
    el.enableDisplayMode();
    el.hide();
    
    // Create the editable field
    var field;
    if (action.fieldType == "textarea") {
      // Create the textarea child
      field = el.insertSibling({
        tag:    'textarea',
        cls:    'x-editable',
        style:  (action.forceHeight) ? 'height:' + elHeight + 'px;' : '',
        name:   action.fieldName || action.name,
        html:   value
      });
    } else {
      // Create the input child
      field = el.insertSibling({
        tag:   'input',
        cls:   'x-editable',
        name:  action.fieldName || action.name,
        type:  'text',
        value: value
      });
    };
    
    // Focus on the field
    field.focus();
    field.dom.select();
    
    // Set the editable field to the new editable field
    this.editableField = {
      record: record,
      field:  field,
      el:     el,
      value:  value,
      action: action
    };
    
    // add events for the field and body
    field.on('keypress', this.checkForEndEditEvent, this);
    field.on('keyup', this.checkForEndEditEvent, this);
    Ext.getBody().on('click', this.checkForEndEditEvent, this);
    Ext.getBody().on('keypress', this.checkForEndEditEvent, this);
  },
  
  /**
   * Method which is called when a user either pressed a key in the editable field
   * or clicks on the body of the window. If it is the ESC key pressed, it will cancel
   * the edit and return the old values. if it is an ENTER press or click on the body
   * (not the field), it will fire off an action on the controller to try and save the
   * field.
   * @param {Ext.Event} e The Ext event object
   */
  checkForEndEditEvent: function(e) {
    if (!this.editableField) { return; };
    
    // Get the field and el
    var record = this.editableField.record;
    var field  = this.editableField.field;
    var el     = this.editableField.el;
    var value  = this.editableField.value;
    var action = this.editableField.action;
    
    // get the key pressed
    var keyPressed = e.getKey() || null;

		// Listen for ESC keypress
    if (keyPressed == e.ESC) {
      return this.endFieldEdit(el, record, field, action, value, true);
    };
		
		// Validate the value
		if (!this.validateValue(field.getValue(), action)) return field.addClass('x-form-invalid');
		else field.removeClass('x-form-invalid');
		
    // Check if the target is the field or not
    if (Ext.get(e.getTarget()) != field) {
      return this.endFieldEdit(el, record, field, action, value);
    };
    
    if (action.fieldType != "textarea") {
      // Listen for Enter keypress
      if (keyPressed == e.RETURN || keyPressed == e.ENTER) {
        return this.endFieldEdit(el, record, field, action, value);
      };
    };
  },
  
  /**
   * Ends the field editing, removes the editable field and shows the original element
   * @param {Ext.Element} el The original element whih needs to be shown again
   * @param {Ext.Record} record The record from the model
   * @param {Ext.Element} field The field which has been created
   * @param {String} value The old value in the field
   * @param {Boolean} esc True if we need to stop the event
   */
  endFieldEdit: function(el, record, field, action, value, esc) {
    // Check if the value has changed
    if (field.getValue() != value && !esc) {
      if (action.name != "tags") el.update(field.getValue());
			
      var updates = {};
      updates[field.dom.name] = field.getValue();
      
      this.fireEvent('update', record, updates, action.forceRefresh);
      
      //TODO: Remove this once Photos and ID cards have been moved across
      // if (this.controller) {
      //   record.set(field.dom.name, field.getValue());
      //   this.controller.fireAction('update-editable', this.controller, [record, action.forceRefresh]);
      // }
    };
		
    // End editing
    this.editing = false;
    
    // Remove the editable field
    this.editableField = null;
    
    // Remove the onbody event
    Ext.getBody().un('click', this.checkForEndEditEvent, this);
    
    // remove the input field
    field.remove();

    // show the element again
    el.show();
  },

	/**
	 * Valies the value of the specifed action
	 * @param {String} value The value that needs to be validated
	 * @param {Object} action The action being validated
	 */
	validateValue: function(value, action) {
		var valid = true;
		
		// Check if the value is blank and allowBlank is false
		if (action.allowBlank === false) {
	    if (value == "" || value[0] == " ") {
	      valid = false;
	    };
		};
    
		// check for maxLength
		if(value.length > this.maxLength) {
			valid = false;
		};
		
		// Validate the value if it has a vtype
    if(action.vtype) {
			var vt = Ext.form.VTypes;
	    if(!vt[action.vtype](value)) valid = false;
			if (value == "" && valid === false && !action.allowBlank) { valid = true; };
		};
		
		return valid;
	},
	 
	initEditableMouseover: function() {
		var dataview = this.dataview;
		if (!dataview) return;
		
		Ext.each((dataview.getNodes) ? dataview.getNodes() : [], function(node) {
			var record = dataview.getRecord(node),
					node	 = Ext.get(node),
					els    = node.select('div');
			
			if (record.get('can_edit')) {
				els.each(function(el) {
					var element = Ext.get(el);
					
					Ext.each(element.dom.className.split(' '), function(cls) {
            // if (cls.split('-')[0] == "action" && cls.split('-')[1] == "edit") {
						if (cls.split('-')[0] == "action") {
						  var cl = cls.split('-')[1];
						  
						  if (cl == "edit" || cl == "filesPicker" || cl == "duePicker" || cl == "assignedPicker") {
  							var win = this.view;
  							if (!win) return;
  							element.on('mouseover', function() {
  								win.fireEvent('statusbar-event-start', Ext.ux.Translator.translate('app_statusbar_double_click_to_edit'));
  							}, element);
  							element.on('mouseout',  function() {
  								win.fireEvent('statusbar-event-stop');
  							});
  							return;
  						};
						};
					}, this);
				}, this);
			};
		}, this);
	}
});
